/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.composer.impl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.opencastproject.composer.api.EncoderException; import org.opencastproject.composer.api.EncodingProfile; import org.opencastproject.composer.api.LaidOutElement; import org.opencastproject.composer.layout.Dimension; import org.opencastproject.composer.layout.HorizontalCoverageLayoutSpec; import org.opencastproject.composer.layout.LayoutManager; import org.opencastproject.composer.layout.MultiShapeLayout; import org.opencastproject.composer.layout.Serializer; import org.opencastproject.inspection.api.MediaInspectionException; import org.opencastproject.inspection.api.MediaInspectionService; import org.opencastproject.job.api.Job; import org.opencastproject.job.api.JobBarrier; import org.opencastproject.mediapackage.Attachment; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.Track; import org.opencastproject.mediapackage.attachment.AttachmentImpl; import org.opencastproject.security.api.DefaultOrganization; import org.opencastproject.security.api.JaxbOrganization; import org.opencastproject.security.api.JaxbRole; import org.opencastproject.security.api.JaxbUser; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.User; import org.opencastproject.security.api.UserDirectoryService; import org.opencastproject.serviceregistry.api.IncidentService; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryInMemoryImpl; import org.opencastproject.util.IoSupport; import org.opencastproject.util.JsonObj; import org.opencastproject.util.MimeType; import org.opencastproject.util.StreamHelper; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; import org.opencastproject.workspace.api.Workspace; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.easymock.EasyMock; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.osgi.framework.BundleContext; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.HashSet; import java.util.List; /** * Tests the {@link ComposerServiceImpl}. */ public class ComposerServiceTest { /** The sources file to test with */ private File source = null; private File sourceVideoOnly = null; private File sourceAudioOnly = null; private File sourceImage = null; /** The composer service to test */ private ComposerServiceImpl composerService = null; /** The service registry for job dispatching */ private ServiceRegistry serviceRegistry = null; /** FFmpeg binary location */ private static final String FFMPEG_BINARY = "ffmpeg"; /** File pointer to the testing dir to not pollute tmp */ private static File testDir = new File("target"); /** True to run the tests */ private static boolean ffmpegInstalled = true; /** True to run the tests */ private static boolean ffmpegInstalledGreaterVersion2 = false; /** Logging facility */ private static final Logger logger = LoggerFactory.getLogger(ComposerServiceTest.class); private Track sourceAudioTrack; private Track sourceVideoTrack; private Track inspectedTrack; /** Encoding profile scanner */ private EncodingProfileScanner profileScanner; @BeforeClass public static void testForFFmpeg() { StreamHelper stdout = null; StreamHelper stderr = null; Process p = null; try { p = new ProcessBuilder(FFMPEG_BINARY, "-version").start(); StringBuffer buffer = new StringBuffer(); stdout = new StreamHelper(p.getInputStream(), buffer); stderr = new StreamHelper(p.getErrorStream()); int status = p.waitFor(); stdout.stopReading(); stderr.stopReading(); logger.info(buffer.toString()); if (status != 0) throw new IllegalStateException(); if (buffer.toString().startsWith("ffmpeg version 2")) ffmpegInstalledGreaterVersion2 = true; } catch (Throwable t) { logger.warn("Skipping image composer service tests due to unsatisifed or erroneus ffmpeg installation"); ffmpegInstalled = false; } finally { IoSupport.closeQuietly(stdout); IoSupport.closeQuietly(stderr); IoSupport.closeQuietly(p); } } private static File getFile(String path) throws Exception { return new File(ComposerServiceTest.class.getResource(path).toURI()); } @Before public void setUp() throws Exception { if (!ffmpegInstalled) return; // Copy an existing media file to a temp file File f = getFile("/slidechanges.mov"); source = File.createTempFile(FilenameUtils.getBaseName(f.getName()), ".mov", testDir); FileUtils.copyFile(f, source); f = null; // Create another video only file f = getFile("/video.mp4"); sourceVideoOnly = File.createTempFile(FilenameUtils.getBaseName(f.getName()), ".mp4", testDir); FileUtils.copyFile(f, sourceVideoOnly); f = null; // Create another audio only file f = getFile("/audio.mp3"); sourceAudioOnly = File.createTempFile(FilenameUtils.getBaseName(f.getName()), ".mp3", testDir); FileUtils.copyFile(f, sourceAudioOnly); f = null; // Create an image file f = getFile("/image.jpg"); sourceImage = File.createTempFile(FilenameUtils.getBaseName(f.getName()), ".jpg", testDir); FileUtils.copyFile(f, sourceImage); f = null; // create the needed mocks BundleContext bc = EasyMock.createNiceMock(BundleContext.class); EasyMock.expect(bc.getProperty((String) EasyMock.anyObject())).andReturn(FFMPEG_BINARY); ComponentContext cc = EasyMock.createNiceMock(ComponentContext.class); EasyMock.expect(cc.getBundleContext()).andReturn(bc).anyTimes(); JaxbOrganization org = new DefaultOrganization(); HashSet<JaxbRole> roles = new HashSet<JaxbRole>(); roles.add(new JaxbRole(DefaultOrganization.DEFAULT_ORGANIZATION_ADMIN, org, "")); User user = new JaxbUser("admin", "test", org, roles); OrganizationDirectoryService orgDirectory = EasyMock.createNiceMock(OrganizationDirectoryService.class); EasyMock.expect(orgDirectory.getOrganization((String) EasyMock.anyObject())).andReturn(org).anyTimes(); UserDirectoryService userDirectory = EasyMock.createNiceMock(UserDirectoryService.class); EasyMock.expect(userDirectory.loadUser("admin")).andReturn(user).anyTimes(); SecurityService securityService = EasyMock.createNiceMock(SecurityService.class); EasyMock.expect(securityService.getOrganization()).andReturn(org).anyTimes(); EasyMock.expect(securityService.getUser()).andReturn(user).anyTimes(); Workspace workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect(workspace.get((URI) EasyMock.anyObject())).andReturn(source).anyTimes(); profileScanner = new EncodingProfileScanner(); File encodingProfile = getFile("/encodingprofiles.properties"); assertNotNull("Encoding profile must exist", encodingProfile); profileScanner.install(encodingProfile); // Finish setting up the mocks EasyMock.replay(bc, cc, orgDirectory, userDirectory, securityService, workspace); // Create an encoding engine factory EncoderEngineFactoryImpl encoderEngineFactory = new EncoderEngineFactoryImpl(); encoderEngineFactory.activate(cc); inspectedTrack = (Track) MediaPackageElementParser.getFromXml(IOUtils.toString( ComposerServiceTest.class.getResourceAsStream("/composer_test_source_track_video.xml"))); sourceAudioTrack = (Track) MediaPackageElementParser.getFromXml(IOUtils.toString( ComposerServiceTest.class.getResourceAsStream("/composer_test_source_track_audio.xml"))); sourceVideoTrack = (Track) MediaPackageElementParser.getFromXml(IOUtils.toString( ComposerServiceTest.class.getResourceAsStream("/composer_test_source_track_video.xml"))); // Create and populate the composer service composerService = new ComposerServiceImpl() { @Override protected Job inspect(Job job, URI workspaceURI) throws EncoderException { Job inspectionJob = EasyMock.createNiceMock(Job.class); try { EasyMock.expect(inspectionJob.getPayload()).andReturn(MediaPackageElementParser.getAsXml(inspectedTrack)); } catch (MediaPackageException e) { throw new RuntimeException(e); } EasyMock.replay(inspectionJob); return inspectionJob; } }; serviceRegistry = new ServiceRegistryInMemoryImpl(composerService, securityService, userDirectory, orgDirectory, EasyMock.createNiceMock(IncidentService.class)); composerService.setEncoderEngineFactory(encoderEngineFactory); composerService.setOrganizationDirectoryService(orgDirectory); composerService.setSecurityService(securityService); composerService.setServiceRegistry(serviceRegistry); composerService.setUserDirectoryService(userDirectory); composerService.setProfileScanner(profileScanner); composerService.setWorkspace(workspace); } @After public void tearDown() throws Exception { FileUtils.deleteQuietly(source); FileUtils.deleteQuietly(sourceVideoOnly); FileUtils.deleteQuietly(sourceAudioOnly); FileUtils.deleteQuietly(sourceImage); } @Test public void testConcurrentExecutionWithSameSource() throws Exception { if (!ffmpegInstalled) return; assertTrue(source.isFile()); Track sourceTrack = (Track) MediaPackageElementParser.getFromXml(IOUtils.toString( ComposerServiceTest.class.getResourceAsStream("/composer_test_source_track1.xml"))); List<Job> jobs = new ArrayList<Job>(); for (int i = 0; i < 10; i++) { jobs.add(composerService.image(sourceTrack, "player-preview.http", 1D)); } boolean success = new JobBarrier(null, serviceRegistry, jobs.toArray(new Job[jobs.size()])).waitForJobs().isSuccess(); assertTrue(success); for (Job j : jobs) { // Always check the service registry for the latest version of the job Job job = serviceRegistry.getJob(j.getId()); assertEquals(Job.Status.FINISHED, job.getStatus()); } } @Test public void testEncode() throws Exception { if (!ffmpegInstalled) return; assertTrue(sourceVideoOnly.isFile()); assertTrue(sourceAudioOnly.isFile()); // Need different media files Workspace workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect(workspace.get((URI) EasyMock.anyObject())).andReturn(sourceVideoOnly).anyTimes(); EasyMock.expect( workspace.putInCollection((String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(sourceVideoOnly.toURI()).anyTimes(); composerService.setWorkspace(workspace); MediaInspectionService inspect = EasyMock.createNiceMock(MediaInspectionService.class); EasyMock.expect(inspect.inspect((URI) EasyMock.anyObject())) .andThrow(new MediaInspectionException("test complete")).anyTimes(); EasyMock.replay(workspace, inspect); try { composerService.encode(sourceVideoTrack, "av.work"); } catch (EncoderException e) { assertTrue("test complete".equals(e.getMessage())); } } @Test public void testEncode2() throws Exception { if (!ffmpegInstalled) return; assertTrue(sourceVideoOnly.isFile()); assertTrue(sourceAudioOnly.isFile()); // Need different media files Workspace workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect(workspace.get((URI) EasyMock.anyObject())).andReturn(sourceVideoOnly).anyTimes(); EasyMock.expect( workspace.putInCollection((String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(sourceVideoOnly.toURI()).anyTimes(); composerService.setWorkspace(workspace); MediaInspectionService inspect = EasyMock.createNiceMock(MediaInspectionService.class); EasyMock.expect(inspect.inspect((URI) EasyMock.anyObject())) .andThrow(new MediaInspectionException("test complete")).anyTimes(); EasyMock.replay(workspace, inspect); try { composerService.encode(null, sourceVideoTrack, sourceAudioTrack, "av.work", null); } catch (IllegalArgumentException e) { assertTrue("The Job parameter must not be null".equals(e.getMessage())); } } @Test public void testParallelEncode() throws Exception { if (!ffmpegInstalled) return; assertTrue(sourceVideoOnly.isFile()); assertTrue(sourceAudioOnly.isFile()); // Need different media files Workspace workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect(workspace.get((URI) EasyMock.anyObject())).andReturn(sourceVideoOnly).anyTimes(); EasyMock.expect( workspace.putInCollection((String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(sourceVideoOnly.toURI()).anyTimes(); composerService.setWorkspace(workspace); MediaInspectionService inspect = EasyMock.createNiceMock(MediaInspectionService.class); EasyMock.expect(inspect.inspect((URI) EasyMock.anyObject())) .andThrow(new MediaInspectionException("test complete")).anyTimes(); EasyMock.replay(workspace, inspect); try { composerService.parallelEncode(sourceVideoTrack, "parallel.http"); } catch (EncoderException e) { assertTrue("test complete".equals(e.getMessage())); } } /** * Test method for * {@link ComposerServiceImpl#composite(Dimension, LaidOutElement, LaidOutElement, Option, String, String)} */ @Test public void testComposite() throws Exception { if (!ffmpegInstalledGreaterVersion2) return; Dimension outputDimension = new Dimension(500, 500); List<HorizontalCoverageLayoutSpec> layouts = new ArrayList<HorizontalCoverageLayoutSpec>(); layouts.add(Serializer.horizontalCoverageLayoutSpec(JsonObj .jsonObj("{\"horizontalCoverage\":1.0,\"anchorOffset\":{\"referring\":{\"left\":1.0,\"top\":1.0},\"offset\":{\"y\":-20,\"x\":-20},\"reference\":{\"left\":1.0,\"top\":1.0}}}"))); layouts.add(Serializer.horizontalCoverageLayoutSpec(JsonObj .jsonObj("{\"horizontalCoverage\":0.2,\"anchorOffset\":{\"referring\":{\"left\":0.0,\"top\":0.0},\"offset\":{\"y\":-20,\"x\":-20},\"reference\":{\"left\":0.0,\"top\":0.0}}}"))); layouts.add(Serializer.horizontalCoverageLayoutSpec(JsonObj .jsonObj("{\"horizontalCoverage\":1.0,\"anchorOffset\":{\"referring\":{\"left\":1.0,\"top\":0.0},\"offset\":{\"y\":20,\"x\":20},\"reference\":{\"left\":1.0,\"top\":0.0}}}"))); List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>(); shapes.add(0, Tuple.tuple(new Dimension(300, 300), layouts.get(0))); shapes.add(1, Tuple.tuple(new Dimension(200, 200), layouts.get(1))); MultiShapeLayout multiShapeLayout = LayoutManager.multiShapeLayout(outputDimension, shapes); Option<LaidOutElement<Attachment>> watermarkOption = Option.<LaidOutElement<Attachment>> none(); LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(sourceVideoTrack, multiShapeLayout.getShapes() .get(0)); LaidOutElement<Track> upperLaiedOutElement = new LaidOutElement<Track>(sourceVideoTrack, multiShapeLayout.getShapes() .get(1)); Job composite = composerService.composite(outputDimension, Option.option(lowerLaidOutElement), upperLaiedOutElement, watermarkOption, "composite.work", "black"); JobBarrier barrier = new JobBarrier(null, serviceRegistry, composite); if (!barrier.waitForJobs().isSuccess()) { Assert.fail("Composite job did not success!"); } Track compositeTrack = (Track) MediaPackageElementParser.getFromXml(composite.getPayload()); Assert.assertNotNull(compositeTrack); inspectedTrack.setIdentifier(compositeTrack.getIdentifier()); inspectedTrack.setMimeType(MimeType.mimeType("video", "mp4")); Assert.assertEquals(inspectedTrack, compositeTrack); } /** * Test method for {@link ComposerServiceImpl#concat(String, Dimension, Track...)} */ @Test public void testConcat() throws Exception { if (!ffmpegInstalledGreaterVersion2) return; Dimension outputDimension = new Dimension(500, 500); Job concat = composerService.concat("concat.work", outputDimension, sourceVideoTrack, sourceVideoTrack); JobBarrier barrier = new JobBarrier(null, serviceRegistry, concat); if (!barrier.waitForJobs().isSuccess()) { Assert.fail("Concat job did not success!"); } Track concatTrack = (Track) MediaPackageElementParser.getFromXml(concat.getPayload()); Assert.assertNotNull(concatTrack); inspectedTrack.setIdentifier(concatTrack.getIdentifier()); inspectedTrack.setMimeType(MimeType.mimeType("video", "mp4")); Assert.assertEquals(inspectedTrack, concatTrack); } /** * Test method for {@link ComposerServiceImpl#concat(String, Dimension, float, Track...)} */ @Test public void testConcatWithFrameRate() throws Exception { if (!ffmpegInstalledGreaterVersion2) { return; } Dimension outputDimension = new Dimension(500, 500); Job concat = composerService.concat("concat.work", outputDimension, 20.0f, sourceVideoTrack, sourceVideoTrack); JobBarrier barrier = new JobBarrier(null, serviceRegistry, concat); if (!barrier.waitForJobs().isSuccess()) { Assert.fail("Concat job did not success!"); } Track concatTrack = (Track) MediaPackageElementParser.getFromXml(concat.getPayload()); Assert.assertNotNull(concatTrack); inspectedTrack.setIdentifier(concatTrack.getIdentifier()); inspectedTrack.setMimeType(MimeType.mimeType("video", "mp4")); Assert.assertEquals(inspectedTrack, concatTrack); } /** * Test method for * {@link org.opencastproject.composer.impl.ComposerServiceImpl#imageToVideo(org.opencastproject.mediapackage.Attachment, String, Long)} */ @Test public void testImageToVideo() throws Exception { if (!ffmpegInstalled) return; assertTrue(sourceImage.isFile()); // Need different media files Workspace workspace = EasyMock.createNiceMock(Workspace.class); EasyMock.expect(workspace.get((URI) EasyMock.anyObject())).andReturn(sourceImage).anyTimes(); EasyMock.expect( workspace.putInCollection((String) EasyMock.anyObject(), (String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(sourceImage.toURI()).anyTimes(); composerService.setWorkspace(workspace); EasyMock.replay(workspace); EncodingProfile imageToVideoProfile = profileScanner.getProfile("image-movie.work"); Attachment attachement = AttachmentImpl.fromURI(sourceImage.toURI()); attachement.setIdentifier("test image"); Job imageToVideo = composerService.imageToVideo(attachement, imageToVideoProfile.getIdentifier(), 2L); JobBarrier barrier = new JobBarrier(null, serviceRegistry, imageToVideo); if (!barrier.waitForJobs().isSuccess()) { Assert.fail("ImageToVideo job did not success!"); } Track imageToVideoTrack = (Track) MediaPackageElementParser.getFromXml(imageToVideo.getPayload()); Assert.assertNotNull(imageToVideoTrack); inspectedTrack.setIdentifier(imageToVideoTrack.getIdentifier()); inspectedTrack.setMimeType(MimeType.mimeType("video", "mp4")); Assert.assertEquals(inspectedTrack, imageToVideoTrack); } }