use of org.opencastproject.mediapackage.Track in project opencast by opencast.
the class CompositeWorkflowOperationHandlerTest method testSingleLayout.
@Test
public void testSingleLayout() throws Exception {
setMockups();
// operation configuration
String targetTags = "engage,compound";
Map<String, String> configurations = new HashMap<String, String>();
configurations.put("source-flavor-upper", "presenter/source");
configurations.put("source-flavor-lower", "presentation/source");
configurations.put("target-tags", targetTags);
configurations.put("target-flavor", "composite/work");
configurations.put("encoding-profile", "composite");
configurations.put("layout", TEST_LAYOUT);
configurations.put("layout-single", TEST_SINGLE_LAYOUT);
configurations.put("output-resolution", "1900x1080");
configurations.put("output-background", "black");
// run the operation handler
WorkflowOperationResult result = getWorkflowOperationResult(mp, configurations);
// check track metadata
MediaPackage mpNew = result.getMediaPackage();
Assert.assertEquals(Action.CONTINUE, result.getAction());
Track trackEncoded = mpNew.getTrack(COMPOUND_TRACK_ID);
Assert.assertEquals("composite/work", trackEncoded.getFlavor().toString());
Assert.assertTrue(Arrays.asList(targetTags.split("\\W")).containsAll(Arrays.asList(trackEncoded.getTags())));
}
use of org.opencastproject.mediapackage.Track in project opencast by opencast.
the class ComposerServiceImpl method encode.
/**
* Encodes audio and video track to a file. If both an audio and a video track are given, they are muxed together into
* one movie container.
*
* @param tracks
* tracks to use for processing
* @param profileId
* the encoding profile
* @param properties
* encoding properties
* @return the encoded track or none if the operation does not return a track. This may happen for example when doing
* two pass encodings where the first pass only creates metadata for the second one
* @throws EncoderException
* if encoding fails
*/
private Option<Track> encode(final Job job, Map<String, Track> tracks, String profileId) throws EncoderException, MediaPackageException {
final String targetTrackId = idBuilder.createNew().toString();
Map<String, File> files = new HashMap<>();
// Get the tracks and make sure they exist
for (Entry<String, Track> track : tracks.entrySet()) {
files.put(track.getKey(), loadTrackIntoWorkspace(job, track.getKey(), track.getValue()));
}
// Get the encoding profile
final EncodingProfile profile = getProfile(job, profileId);
List<String> trackMsg = new LinkedList<>();
for (Entry<String, Track> track : tracks.entrySet()) {
trackMsg.add(String.format("%s: %s", track.getKey(), track.getValue().getIdentifier()));
}
logger.info("Encoding {} into {} using profile {}", StringUtils.join(trackMsg, ", "), targetTrackId, profileId);
// Do the work
final EncoderEngine encoder = getEncoderEngine();
List<File> output;
try {
output = encoder.process(files, profile, null);
} catch (EncoderException e) {
Map<String, String> params = new HashMap<>();
for (Entry<String, Track> track : tracks.entrySet()) {
params.put(track.getKey(), track.getValue().getIdentifier());
}
params.put("profile", profile.getIdentifier());
params.put("properties", "EMPTY");
incident().recordFailure(job, ENCODING_FAILED, e, params, detailsFor(e, encoder));
throw e;
} finally {
activeEncoder.remove(encoder);
}
// We expect zero or one file as output
if (output.size() == 0) {
return none();
} else if (output.size() != 1) {
// Ensure we do not leave behind old files in the workspace
for (File file : output) {
FileUtils.deleteQuietly(file);
}
throw new EncoderException("Composite does not support multiple files as output");
}
// Put the file in the workspace
URI workspaceURI = putToCollection(job, output.get(0), "encoded file");
// Have the encoded track inspected and return the result
Job inspectionJob = inspect(job, workspaceURI);
Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload());
inspectedTrack.setIdentifier(targetTrackId);
if (profile.getMimeType() != null)
inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType()));
return some(inspectedTrack);
}
use of org.opencastproject.mediapackage.Track in project opencast by opencast.
the class ComposerServiceImpl method concat.
private Option<Track> concat(Job job, List<Track> tracks, String profileId, Dimension outputDimension, float outputFrameRate) throws EncoderException, MediaPackageException {
if (tracks.size() < 2) {
Map<String, String> params = new HashMap<>();
params.put("tracks-size", Integer.toString(tracks.size()));
params.put("tracks", StringUtils.join(tracks, ","));
incident().recordFailure(job, CONCAT_LESS_TRACKS, params);
throw new EncoderException("The track parameter must at least have two tracks present");
}
boolean onlyAudio = true;
for (Track t : tracks) {
if (t.hasVideo()) {
onlyAudio = false;
break;
}
}
if (!onlyAudio && outputDimension == null) {
Map<String, String> params = new HashMap<>();
params.put("tracks", StringUtils.join(tracks, ","));
incident().recordFailure(job, CONCAT_NO_DIMENSION, params);
throw new EncoderException("The output dimension id parameter must not be null when concatenating video");
}
final String targetTrackId = idBuilder.createNew().toString();
// Get the tracks and make sure they exist
List<File> trackFiles = new ArrayList<>();
int i = 0;
for (Track track : tracks) {
if (!track.hasAudio() && !track.hasVideo()) {
Map<String, String> params = new HashMap<>();
params.put("track-id", track.getIdentifier());
params.put("track-url", track.getURI().toString());
incident().recordFailure(job, NO_STREAMS, params);
throw new EncoderException("Track has no audio or video stream available: " + track);
}
trackFiles.add(i++, loadTrackIntoWorkspace(job, "concat", track));
}
// Create the engine
final EncoderEngine encoderEngine = getEncoderEngine();
if (onlyAudio) {
logger.info("Concatenating audio tracks {} into {}", trackFiles, targetTrackId);
} else {
logger.info("Concatenating video tracks {} into {}", trackFiles, targetTrackId);
}
// Get the encoding profile
EncodingProfile profile = getProfile(job, profileId);
// Creating video filter command for concat
final String concatCommand = buildConcatCommand(onlyAudio, outputDimension, outputFrameRate, trackFiles, tracks);
Map<String, String> properties = new HashMap<>();
properties.put(EncoderEngine.CMD_SUFFIX + ".concatCommand", concatCommand);
File output;
try {
output = encoderEngine.encode(trackFiles.get(0), profile, properties);
} catch (EncoderException e) {
Map<String, String> params = new HashMap<>();
List<String> trackList = new ArrayList<>();
for (Track t : tracks) {
trackList.add(t.getURI().toString());
}
params.put("tracks", StringUtils.join(trackList, ","));
params.put("profile", profile.getIdentifier());
params.put("properties", properties.toString());
incident().recordFailure(job, CONCAT_FAILED, e, params, detailsFor(e, encoderEngine));
throw e;
} finally {
activeEncoder.remove(encoderEngine);
}
// concat did not return a file
if (!output.exists() || output.length() == 0)
return none();
// Put the file in the workspace
URI workspaceURI = putToCollection(job, output, "concatenated file");
// Have the concat track inspected and return the result
Job inspectionJob = inspect(job, workspaceURI);
Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload());
inspectedTrack.setIdentifier(targetTrackId);
if (profile.getMimeType() != null)
inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType()));
return some(inspectedTrack);
}
use of org.opencastproject.mediapackage.Track in project opencast by opencast.
the class ComposerRestService method image.
/**
* Encodes a track in a media package.
*
* @param sourceTrackXml
* The source track
* @param profileId
* The profile to use in encoding this track
* @param times
* one or more times in seconds separated by comma
* @return A {@link Response} with the resulting track in the response body
* @throws Exception
*/
@POST
@Path("image")
@Produces(MediaType.TEXT_XML)
@RestQuery(name = "image", description = "Starts an image extraction process, based on the specified encoding profile ID and the source track", restParameters = { @RestParameter(description = "The track containing the video stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = "${this.videoTrackDefault}"), @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "player-preview.http"), @RestParameter(description = "The number of seconds (many numbers can be specified, separated by semicolon) into the video to extract the image", isRequired = false, name = "time", type = Type.STRING), @RestParameter(description = "An optional set of key=value\\n properties", isRequired = false, name = "properties", type = TEXT) }, reponses = { @RestResponse(description = "Results in an xml document containing the image attachment", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "The image extraction job")
public Response image(@FormParam("sourceTrack") String sourceTrackXml, @FormParam("profileId") String profileId, @FormParam("time") String times, @FormParam("properties") LocalHashMap localMap) throws Exception {
// Ensure that the POST parameters are present
if (StringUtils.isBlank(sourceTrackXml) || StringUtils.isBlank(profileId))
return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
// Deserialize the source track
MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackXml);
if (!Track.TYPE.equals(sourceTrack.getElementType()))
return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
boolean timeBased = false;
double[] timeArray = null;
if (StringUtils.isNotBlank(times)) {
// parse time codes
try {
timeArray = parseTimeArray(times);
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST).entity("could not parse times: invalid format").build();
}
timeBased = true;
} else if (localMap == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
try {
// Asynchronously encode the specified tracks
Job job;
if (timeBased) {
job = composerService.image((Track) sourceTrack, profileId, timeArray);
} else {
job = composerService.image((Track) sourceTrack, profileId, localMap.getMap());
}
return Response.ok().entity(new JaxbJob(job)).build();
} catch (EncoderException e) {
logger.warn("Unable to extract image(s): " + e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
use of org.opencastproject.mediapackage.Track in project opencast by opencast.
the class ComposerRestService method composite.
/**
* Compose two videos into one with an optional watermark.
*
* @param compositeSizeJson
* The composite track dimension as JSON
* @param lowerTrackXml
* The lower track of the composition as XML
* @param lowerLayoutJson
* The lower layout as JSON
* @param upperTrackXml
* The upper track of the composition as XML
* @param upperLayoutJson
* The upper layout as JSON
* @param watermarkAttachmentXml
* The watermark image attachment of the composition as XML
* @param watermarkLayoutJson
* The watermark layout as JSON
* @param profileId
* The encoding profile to use
* @param background
* The background color
* @return A {@link Response} with the resulting track in the response body
* @throws Exception
*/
@POST
@Path("composite")
@Produces(MediaType.TEXT_XML)
@RestQuery(name = "composite", description = "Starts a video compositing process, based on the specified resolution, encoding profile ID, the source elements and their layouts", restParameters = { @RestParameter(description = "The resolution size of the resulting video as JSON", isRequired = true, name = "compositeSize", type = Type.STRING), @RestParameter(description = "The lower source track containing the lower video", isRequired = true, name = "lowerTrack", type = Type.TEXT), @RestParameter(description = "The lower layout containing the JSON definition of the layout", isRequired = true, name = "lowerLayout", type = Type.TEXT), @RestParameter(description = "The upper source track containing the upper video", isRequired = false, name = "upperTrack", type = Type.TEXT), @RestParameter(description = "The upper layout containing the JSON definition of the layout", isRequired = false, name = "upperLayout", type = Type.TEXT), @RestParameter(description = "The watermark source attachment containing watermark image", isRequired = false, name = "watermarkTrack", type = Type.TEXT), @RestParameter(description = "The watermark layout containing the JSON definition of the layout", isRequired = false, name = "watermarkLayout", type = Type.TEXT), @RestParameter(description = "The background color", isRequired = false, name = "background", type = Type.TEXT, defaultValue = "black"), @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING) }, reponses = { @RestResponse(description = "Results in an xml document containing the compound video track", responseCode = HttpServletResponse.SC_OK), @RestResponse(description = "If required parameters aren't set or if the source elements aren't from the right type", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
public Response composite(@FormParam("compositeSize") String compositeSizeJson, @FormParam("lowerTrack") String lowerTrackXml, @FormParam("lowerLayout") String lowerLayoutJson, @FormParam("upperTrack") String upperTrackXml, @FormParam("upperLayout") String upperLayoutJson, @FormParam("watermarkAttachment") String watermarkAttachmentXml, @FormParam("watermarkLayout") String watermarkLayoutJson, @FormParam("profileId") String profileId, @FormParam("background") @DefaultValue("black") String background) throws Exception {
// Ensure that the POST parameters are present
if (StringUtils.isBlank(compositeSizeJson) || StringUtils.isBlank(lowerTrackXml) || StringUtils.isBlank(lowerLayoutJson) || StringUtils.isBlank(profileId))
return Response.status(Response.Status.BAD_REQUEST).entity("One of the required parameters must not be null").build();
// Deserialize the source elements
MediaPackageElement lowerTrack = MediaPackageElementParser.getFromXml(lowerTrackXml);
Layout lowerLayout = Serializer.layout(JsonObj.jsonObj(lowerLayoutJson));
if (!Track.TYPE.equals(lowerTrack.getElementType()))
return Response.status(Response.Status.BAD_REQUEST).entity("lowerTrack element must be of type track").build();
LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>((Track) lowerTrack, lowerLayout);
Option<LaidOutElement<Track>> upperLaidOutElement = Option.<LaidOutElement<Track>>none();
if (StringUtils.isNotBlank(upperTrackXml)) {
MediaPackageElement upperTrack = MediaPackageElementParser.getFromXml(upperTrackXml);
Layout upperLayout = Serializer.layout(JsonObj.jsonObj(upperLayoutJson));
if (!Track.TYPE.equals(upperTrack.getElementType())) {
return Response.status(Response.Status.BAD_REQUEST).entity("upperTrack element must be of type track").build();
}
upperLaidOutElement = Option.option(new LaidOutElement<Track>((Track) upperTrack, upperLayout));
}
Option<LaidOutElement<Attachment>> watermarkLaidOutElement = Option.<LaidOutElement<Attachment>>none();
if (StringUtils.isNotBlank(watermarkAttachmentXml)) {
Layout watermarkLayout = Serializer.layout(JsonObj.jsonObj(watermarkLayoutJson));
MediaPackageElement watermarkAttachment = MediaPackageElementParser.getFromXml(watermarkAttachmentXml);
if (!Attachment.TYPE.equals(watermarkAttachment.getElementType()))
return Response.status(Response.Status.BAD_REQUEST).entity("watermarkTrack element must be of type track").build();
watermarkLaidOutElement = Option.some(new LaidOutElement<Attachment>((Attachment) watermarkAttachment, watermarkLayout));
}
Dimension compositeTrackSize = Serializer.dimension(JsonObj.jsonObj(compositeSizeJson));
try {
// Asynchronously composite the specified source elements
Job job = composerService.composite(compositeTrackSize, upperLaidOutElement, lowerLaidOutElement, watermarkLaidOutElement, profileId, background);
return Response.ok().entity(new JaxbJob(job)).build();
} catch (EncoderException e) {
logger.warn("Unable to composite video: " + e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
Aggregations