Skip to content

Commit

Permalink
NXP-29815: add an endpoint to recompute video renditions
Browse files Browse the repository at this point in the history
  • Loading branch information
charlesboidot authored and kevinleturc committed Jun 14, 2021
1 parent da52a22 commit b56b270
Show file tree
Hide file tree
Showing 115 changed files with 793 additions and 4 deletions.
2 changes: 1 addition & 1 deletion modules/platform/pom.xml
Expand Up @@ -85,7 +85,7 @@
<module>nuxeo-platform-userworkspace</module>
<module>nuxeo-platform-versioning-api</module>
<module>nuxeo-platform-versioning-core</module>
<module>nuxeo-platform-video</module>
<module>video</module>
<module>nuxeo-platform-virtualnavigation-core-contrib</module>
<module>nuxeo-platform-web-common</module>
<module>nuxeo-platform-webapp-types</module>
Expand Down
File renamed without changes.
35 changes: 35 additions & 0 deletions modules/platform/video/nuxeo-platform-video-rest/pom.xml
@@ -0,0 +1,35 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-video-parent</artifactId>
<version>11.5-SNAPSHOT</version>
</parent>

<artifactId>nuxeo-platform-video-rest</artifactId>
<name>Nuxeo Platform Video REST</name>
<description>Management API endpoint used to recompute video renditions</description>

<dependencies>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-video</artifactId>
</dependency>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-rest-api-server</artifactId>
</dependency>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-video</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-rest-api-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,80 @@
/*
* (C) Copyright 2021 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* Contributors:
* Charles Boidot
*/
package org.nuxeo.ecm.restapi.server.jaxrs.management;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.SYSTEM_USERNAME;
import static org.nuxeo.ecm.platform.video.computation.RecomputeTranscodedVideosComputation.PARAM_CONVERSION_NAMES;
import static org.nuxeo.ecm.platform.video.computation.RecomputeTranscodedVideosComputation.PARAM_XPATH;
import static org.nuxeo.ecm.platform.video.computation.RecomputeVideoInfoComputation.ONLY_RECOMPUTE_MISSING_VIDEO_INFO;

import java.io.Serializable;
import java.util.List;

import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.nuxeo.ecm.core.bulk.BulkService;
import org.nuxeo.ecm.core.bulk.message.BulkCommand;
import org.nuxeo.ecm.core.bulk.message.BulkStatus;
import org.nuxeo.ecm.platform.video.action.RecomputeVideoConversionsAction;
import org.nuxeo.ecm.webengine.model.WebObject;
import org.nuxeo.ecm.webengine.model.impl.AbstractResource;
import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl;
import org.nuxeo.runtime.api.Framework;

/**
* @since 11.5
*/
@WebObject(type = ManagementObject.MANAGEMENT_OBJECT_PREFIX + "videos")
@Produces(APPLICATION_JSON)
public class VideosObject extends AbstractResource<ResourceTypeImpl> {

// By default we only recompute renditions for document without any renditions
public static final String VIDEOS_DEFAULT_QUERY = "SELECT * FROM Document WHERE ecm:mixinType = 'Video' AND ecm:isProxy = 0 AND ecm:isVersion = 0 AND vid:transcodedVideos/0/name IS NULL";

/**
* Recomputes video renditions for the documents matching the given query or {@link #VIDEOS_DEFAULT_QUERY} if not
* provided.
*
* @param query a custom query to specify which videos should be processed
* @return the {@link BulkStatus} of the command
*/
@POST
@Path("recompute")
public BulkStatus doPostVideos(@FormParam("query") String query,
@FormParam("conversionNames") List<String> conversionNames,
@FormParam("recomputeAllVideoInfo") Boolean recomputeAllVideoInfo) {
String finalQuery = StringUtils.defaultIfBlank(query, VIDEOS_DEFAULT_QUERY);
Boolean onlyRecomputeMissingVideoInfo = !BooleanUtils.toBooleanDefaultIfNull(recomputeAllVideoInfo, false);
BulkService bulkService = Framework.getService(BulkService.class);
String commandId = bulkService.submit(new BulkCommand.Builder(RecomputeVideoConversionsAction.ACTION_NAME,
finalQuery, SYSTEM_USERNAME).repository(ctx.getCoreSession().getRepositoryName())
.param(PARAM_XPATH, "file:content")
.param(ONLY_RECOMPUTE_MISSING_VIDEO_INFO, onlyRecomputeMissingVideoInfo)
.param(PARAM_CONVERSION_NAMES, (Serializable) conversionNames)
.build());
return bulkService.getStatus(commandId);
}

}
@@ -0,0 +1,7 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 1
Bundle-Name: Nuxeo Video REST
Bundle-SymbolicName: org.nuxeo.ecm.platform.video.rest;singleton:=true
Fragment-Host: org.nuxeo.ecm.platform.restapi.server
Bundle-Vendor: Nuxeo
Bundle-Version: 1.0.0
@@ -0,0 +1,229 @@
/*
* (C) Copyright 2021 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* Contributors:
* Charles Boidot
*/

package org.nuxeo.ecm.restapi.server.jaxrs.management;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.nuxeo.ecm.core.bulk.io.BulkConstants.STATUS_ERROR_COUNT;
import static org.nuxeo.ecm.core.bulk.io.BulkConstants.STATUS_ERROR_MESSAGE;
import static org.nuxeo.ecm.core.bulk.io.BulkConstants.STATUS_HAS_ERROR;
import static org.nuxeo.ecm.core.bulk.io.BulkConstants.STATUS_PROCESSED;
import static org.nuxeo.ecm.core.bulk.io.BulkConstants.STATUS_TOTAL;
import static org.nuxeo.ecm.platform.video.VideoConstants.TRANSCODED_VIDEOS_PROPERTY;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.ws.rs.core.MultivaluedMap;

import org.junit.Before;
import org.junit.Test;
import org.nuxeo.common.utils.FileUtils;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.platform.video.VideoFeature;
import org.nuxeo.ecm.platform.video.listener.VideoChangedListener;
import org.nuxeo.ecm.platform.video.service.VideoService;
import org.nuxeo.ecm.restapi.test.ManagementBaseTest;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.Features;

import com.fasterxml.jackson.databind.JsonNode;
import com.sun.jersey.core.util.MultivaluedMapImpl;

/**
* @since 11.5
*/
@Features(VideoFeature.class)
@Deploy("org.nuxeo.ecm.platform.video.rest")
public class TestVideosObject extends ManagementBaseTest {

@Inject
protected CoreSession session;

@Inject
protected VideoService videoService = Framework.getService(VideoService.class);

protected DocumentRef docRef;

@Before
public void createDocument() throws IOException {
DocumentModel doc = session.createDocumentModel("/", "videoDoc", "Video");
Blob blob = Blobs.createBlob(FileUtils.getResourceFileFromContext("videos/video.mpg"), null,
StandardCharsets.UTF_8.name(), "video.mpg");
doc.setPropertyValue("file:content", (Serializable) blob);
doc.putContextData(VideoChangedListener.DISABLE_VIDEO_CONVERSIONS_GENERATION_LISTENER, true);
doc = session.createDocument(doc);
docRef = doc.getRef();

}

@Test
public void testRecomputeVideosInvalidQuery() throws IOException {
String query = "SELECT * FROM nowhere";
doTestRecomputeVideos(query, null, false, false);
}

@Test
public void testRecomputeVideosNoQueryNoConversions() throws IOException {
doTestRecomputeVideos(null, null, false, true);
}

@Test
public void testRecomputeVideosValidQueryCustomConversion() throws IOException {
String query = "SELECT * FROM Document WHERE ecm:mixinType = 'Video'";
doTestRecomputeVideos(query, List.of("WebM 480p"), false, true);
}

@Test
public void testRecomputeVideosImpossibleConversion() throws IOException {
doTestRecomputeVideos(null, List.of("foo 480p"), true, false);
}

@Test
public void testRecomputeVideosCustomRenditionsList() throws IOException {
doTestRecomputeVideos(null, List.of("WebM 480p", "MP4 480p"), false, true);
}

@Test
public void testRecomputeOneAfterRecomputeAll() throws IOException {
// generating all default video renditions
doTestRecomputeVideos(null, null, false, true);

MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
String commandId;

// try recomputing only the Ogg conversion
formData.add("conversionNames", "Ogg 480p");
try (CloseableClientResponse response = httpClientRule.post("/management/videos/recompute/", formData);
InputStream entityStream = response.getEntityInputStream()) {
assertEquals(SC_OK, response.getStatus());
JsonNode node = mapper.readTree(entityStream);
assertBulkStatusScheduled(node);
commandId = getBulkCommandId(node);
}
// waiting for the asynchronous video renditions recompute task
txFeature.nextTransaction();

try (CloseableClientResponse response = httpClientRule.get("/management/bulk/" + commandId);
InputStream entityStream = response.getEntityInputStream()) {
JsonNode node = mapper.readTree(entityStream);
assertEquals(SC_OK, response.getStatus());
assertBulkStatusCompleted(node);
DocumentModel doc = session.getDocument(docRef);
@SuppressWarnings("unchecked")
var transcodedVideos = (List<Map<String, Serializable>>) doc.getPropertyValue(TRANSCODED_VIDEOS_PROPERTY);
assertTranscodedVideos(null, transcodedVideos);
}
}

protected void doTestRecomputeVideos(String query, List<String> expectedRenditions,
boolean expectMissingConversionError, boolean expectSuccess) throws IOException {
// Test there is no already generated renditions
DocumentModel doc = session.getDocument(docRef);

@SuppressWarnings("unchecked")
var transcodedVideos = (List<Map<String, Serializable>>) doc.getPropertyValue(TRANSCODED_VIDEOS_PROPERTY);
assertTrue(transcodedVideos.isEmpty());

// generating new video renditions
MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
if (query != null) {
formData.add("query", query);
}
if (expectedRenditions != null) {
formData.put("conversionNames", expectedRenditions);
}

String commandId;
try (CloseableClientResponse response = httpClientRule.post("/management/videos/recompute/", formData);
InputStream entityStream = response.getEntityInputStream()) {
if (expectMissingConversionError) {
assertEquals(SC_BAD_REQUEST, response.getStatus());
return;
}
assertEquals(SC_OK, response.getStatus());
JsonNode node = mapper.readTree(entityStream);
assertBulkStatusScheduled(node);
commandId = getBulkCommandId(node);
}

// waiting for the asynchronous video renditions recompute task
txFeature.nextTransaction();
assertResponse(commandId, expectedRenditions, expectSuccess);

}

protected void assertResponse(String commandId, List<String> expectedRenditions, boolean expectSuccess)
throws IOException {
try (CloseableClientResponse response = httpClientRule.get("/management/bulk/" + commandId);
InputStream entityStream = response.getEntityInputStream()) {
JsonNode node = mapper.readTree(entityStream);
assertEquals(SC_OK, response.getStatus());
assertBulkStatusCompleted(node);
DocumentModel doc = session.getDocument(docRef);

@SuppressWarnings("unchecked")
var transcodedVideos = (List<Map<String, Serializable>>) doc.getPropertyValue(TRANSCODED_VIDEOS_PROPERTY);
assertNotNull(transcodedVideos);
if (expectSuccess) {
assertEquals(1, node.get(STATUS_PROCESSED).asInt());
assertFalse(node.get(STATUS_HAS_ERROR).asBoolean());
assertEquals(0, node.get(STATUS_ERROR_COUNT).asInt());
assertEquals(1, node.get(STATUS_TOTAL).asInt());
assertTranscodedVideos(expectedRenditions, transcodedVideos);
} else {
assertEquals(0, node.get(STATUS_PROCESSED).asInt());
assertTrue(node.get(STATUS_HAS_ERROR).asBoolean());
assertEquals(1, node.get(STATUS_ERROR_COUNT).asInt());
assertEquals(0, node.get(STATUS_TOTAL).asInt());
assertEquals("Invalid query", node.get(STATUS_ERROR_MESSAGE).asText());
assertTrue(transcodedVideos.isEmpty());
}
}
}

protected void assertTranscodedVideos(List<String> expectedRenditions,
List<Map<String, Serializable>> transcodedVideos) {
if (expectedRenditions == null) {
expectedRenditions = videoService.getAvailableVideoConversionsNames();
}
int nbExpectedRenditions = expectedRenditions.size();
assertEquals(nbExpectedRenditions, transcodedVideos.size());
for (int i = 0; i < nbExpectedRenditions; i++) {
assertEquals(expectedRenditions.get(i), transcodedVideos.get(i).get("name"));
}
}

}
@@ -0,0 +1 @@
Bundle-SymbolicName: org.nuxeo.ecm.platform.video.rest.tests
@@ -1,14 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-parent</artifactId>
<artifactId>nuxeo-platform-video-parent</artifactId>
<version>11.5-SNAPSHOT</version>
</parent>

<artifactId>nuxeo-platform-video</artifactId>
<name>Nuxeo Platform Video</name>
<description>Nuxeo Platform Video provides video management to Nuxeo Web Platform and RCP.</description>
<name>Nuxeo Platform Video Core</name>
<description>Nuxeo Platform Video provides video management for the Nuxeo Platform.</description>

<dependencies>
<dependency>
Expand Down
2 changes: 2 additions & 0 deletions modules/platform/video/nuxeo-platform-video/src/crowdin.ini
@@ -0,0 +1,2 @@
[nuxeo]
en_US=src/main/resources/OSGI-INF/l10n/messages_en_US.properties

0 comments on commit b56b270

Please sign in to comment.