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.composer.impl.endpoint;
23
24 import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
25
26 import org.opencastproject.composer.api.ComposerService;
27 import org.opencastproject.composer.api.EncoderException;
28 import org.opencastproject.composer.api.EncodingProfile;
29 import org.opencastproject.composer.api.EncodingProfileImpl;
30 import org.opencastproject.composer.api.EncodingProfileList;
31 import org.opencastproject.composer.api.LaidOutElement;
32 import org.opencastproject.composer.layout.Dimension;
33 import org.opencastproject.composer.layout.Layout;
34 import org.opencastproject.composer.layout.Serializer;
35 import org.opencastproject.job.api.JaxbJob;
36 import org.opencastproject.job.api.Job;
37 import org.opencastproject.job.api.JobProducer;
38 import org.opencastproject.mediapackage.Attachment;
39 import org.opencastproject.mediapackage.MediaPackageElement;
40 import org.opencastproject.mediapackage.MediaPackageElementParser;
41 import org.opencastproject.mediapackage.Track;
42 import org.opencastproject.rest.AbstractJobProducerEndpoint;
43 import org.opencastproject.serviceregistry.api.ServiceRegistry;
44 import org.opencastproject.smil.api.SmilService;
45 import org.opencastproject.smil.entity.api.Smil;
46 import org.opencastproject.util.JsonObj;
47 import org.opencastproject.util.LocalHashMap;
48 import org.opencastproject.util.NotFoundException;
49 import org.opencastproject.util.UrlSupport;
50 import org.opencastproject.util.data.Option;
51 import org.opencastproject.util.doc.rest.RestParameter;
52 import org.opencastproject.util.doc.rest.RestParameter.Type;
53 import org.opencastproject.util.doc.rest.RestQuery;
54 import org.opencastproject.util.doc.rest.RestResponse;
55 import org.opencastproject.util.doc.rest.RestService;
56
57 import org.apache.commons.lang3.StringUtils;
58 import org.apache.commons.lang3.math.NumberUtils;
59 import org.osgi.service.component.ComponentContext;
60 import org.osgi.service.component.annotations.Component;
61 import org.osgi.service.component.annotations.Reference;
62 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.HashMap;
69 import java.util.LinkedList;
70 import java.util.List;
71 import java.util.Map;
72
73 import javax.servlet.http.HttpServletResponse;
74 import javax.ws.rs.DefaultValue;
75 import javax.ws.rs.FormParam;
76 import javax.ws.rs.GET;
77 import javax.ws.rs.POST;
78 import javax.ws.rs.Path;
79 import javax.ws.rs.PathParam;
80 import javax.ws.rs.Produces;
81 import javax.ws.rs.core.MediaType;
82 import javax.ws.rs.core.Response;
83
84
85
86
87 @Path("/composer/ffmpeg")
88 @RestService(name = "composer", title = "Composer", abstractText = "This service creates and augments Opencast media packages that include media tracks, metadata "
89 + "catalogs and attachments.", notes = {
90 "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
91 "If the service is down or not working it will return a status 503, this means the the underlying service is "
92 + "not working and is either restarting or has failed",
93 "A status code 500 means a general failure has occurred which is not recoverable and was not anticipated. In "
94 + "other words, there is a bug! You should file an error report with your server logs from the time when the "
95 + "error occurred: <a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>" })
96 @Component(
97 property = {
98 "service.description=Composer REST Endpoint",
99 "opencast.service.type=org.opencastproject.composer",
100 "opencast.service.path=/composer/ffmpeg",
101 "opencast.service.jobproducer=true"
102 },
103 immediate = true,
104 service = ComposerRestService.class
105 )
106 @JaxrsResource
107 public class ComposerRestService extends AbstractJobProducerEndpoint {
108
109
110 private static final Logger logger = LoggerFactory.getLogger(ComposerRestService.class);
111
112 private static final String VIDEO_TRACK_DEFAULT = "<track id=\"track-1\" type=\"presentation/source\">\n"
113 + " <mimetype>video/quicktime</mimetype>\n"
114 + " <url>http://localhost:8080/workflow/samples/camera.mpg</url>\n"
115 + " <checksum type=\"md5\">43b7d843b02c4a429b2f547a4f230d31</checksum>\n"
116 + " <duration>14546</duration>\n" + " <video>\n"
117 + " <device type=\"UFG03\" version=\"30112007\" vendor=\"Unigraf\" />\n"
118 + " <encoder type=\"H.264\" version=\"7.4\" vendor=\"Apple Inc\" />\n"
119 + " <resolution>640x480</resolution>\n"
120 + " <scanType type=\"progressive\" />\n"
121 + " <bitrate>540520</bitrate>\n"
122 + " <frameRate>2</frameRate>\n"
123 + " </video>\n"
124 + "</track>";
125
126 private static final String AUDIO_TRACK_DEFAULT = "<track id=\"track-2\" type=\"presentation/source\">\n"
127 + " <mimetype>audio/mp3</mimetype>\n"
128 + " <url>serverUrl/workflow/samples/audio.mp3</url>\n"
129 + " <checksum type=\"md5\">950f9fa49caa8f1c5bbc36892f6fd062</checksum>\n"
130 + " <duration>10472</duration>\n"
131 + " <audio>\n"
132 + " <channels>2</channels>\n"
133 + " <bitdepth>0</bitdepth>\n"
134 + " <bitrate>128004.0</bitrate>\n"
135 + " <samplingrate>44100</samplingrate>\n"
136 + " </audio>\n"
137 + "</track>";
138
139 private static final String IMAGE_ATTACHMENT_DEFAULT = "<attachment id=\"track-3\">\n"
140 + " <mimetype>image/jpeg</mimetype>\n"
141 + " <url>serverUrl/workflow/samples/image.jpg</url>\n"
142 + "</attachment>";
143
144
145 protected String serverUrl;
146
147
148 protected ComposerService composerService = null;
149
150
151 protected ServiceRegistry serviceRegistry = null;
152
153
154 protected SmilService smilService = null;
155
156 @Reference
157 public void setSmilService(SmilService smilService) {
158 this.smilService = smilService;
159 }
160
161
162
163
164
165
166
167 @Reference
168 protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
169 this.serviceRegistry = serviceRegistry;
170 }
171
172
173
174
175
176
177
178 @Reference
179 public void setComposerService(ComposerService composerService) {
180 this.composerService = composerService;
181 }
182
183
184
185
186
187
188
189 public void activate(ComponentContext cc) {
190 if (cc == null || cc.getBundleContext().getProperty("org.opencastproject.server.url") == null) {
191 serverUrl = UrlSupport.DEFAULT_BASE_URL;
192 } else {
193 serverUrl = cc.getBundleContext().getProperty("org.opencastproject.server.url");
194 }
195 }
196
197
198
199
200
201
202
203
204
205
206
207 @POST
208 @Path("encode")
209 @Produces(MediaType.TEXT_XML)
210 @RestQuery(name = "encode", description = "Starts an encoding process, based on the specified encoding profile ID and the track", restParameters = {
211 @RestParameter(description = "The track containing the stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
212 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "mp4-medium.http")
213 }, responses = {
214 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK),
215 @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
216 public Response encode(@FormParam("sourceTrack") String sourceTrackAsXml, @FormParam("profileId") String profileId)
217 throws Exception {
218
219 if (StringUtils.isBlank(sourceTrackAsXml) || StringUtils.isBlank(profileId))
220 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
221
222
223 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackAsXml);
224 if (!Track.TYPE.equals(sourceTrack.getElementType()))
225 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
226
227 try {
228
229 Job job = composerService.encode((Track) sourceTrack, profileId);
230 return Response.ok().entity(new JaxbJob(job)).build();
231 } catch (EncoderException e) {
232 logger.warn("Unable to encode the track: " + e.getMessage());
233 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
234 }
235 }
236
237
238
239
240
241
242
243
244
245
246
247 @POST
248 @Path("parallelencode")
249 @Produces(MediaType.TEXT_XML)
250 @RestQuery(name = "parallelencode", description = "Starts an encoding process, based on the specified encoding profile ID and the track",
251 restParameters = {
252 @RestParameter(description = "The track containing the stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
253 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "mp4-medium.http")
254 }, responses = {
255 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK)
256 }, returnDescription = "")
257 public Response parallelencode(@FormParam("sourceTrack") String sourceTrackAsXml, @FormParam("profileId") String profileId)
258 throws Exception {
259
260 if (sourceTrackAsXml == null || profileId == null) {
261 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
262 }
263
264
265 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackAsXml);
266 if (!Track.TYPE.equals(sourceTrack.getElementType())) {
267 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
268 }
269
270
271 Job job = composerService.parallelEncode((Track) sourceTrack, profileId);
272 if (job == null)
273 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Encoding failed").build();
274 return Response.ok().entity(new JaxbJob(job)).build();
275 }
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291 @POST
292 @Path("trim")
293 @Produces(MediaType.TEXT_XML)
294 @RestQuery(name = "trim", description = "Starts a trimming process, based on the specified track, start time and duration in ms", restParameters = {
295 @RestParameter(description = "The track containing the stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
296 @RestParameter(description = "The encoding profile to use for trimming", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "trim.work"),
297 @RestParameter(description = "The start time in milisecond", isRequired = true, name = "start", type = Type.STRING, defaultValue = "0"),
298 @RestParameter(description = "The duration in milisecond", isRequired = true, name = "duration", type = Type.STRING, defaultValue = "10000") }, responses = {
299 @RestResponse(description = "Results in an xml document containing the job for the trimming task", responseCode = HttpServletResponse.SC_OK),
300 @RestResponse(description = "If the start time is negative or exceeds the track duration", responseCode = HttpServletResponse.SC_BAD_REQUEST),
301 @RestResponse(description = "If the duration is negative or, including the new start time, exceeds the track duration", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
302 public Response trim(@FormParam("sourceTrack") String sourceTrackAsXml, @FormParam("profileId") String profileId,
303 @FormParam("start") long start, @FormParam("duration") long duration) throws Exception {
304
305 if (StringUtils.isBlank(sourceTrackAsXml) || StringUtils.isBlank(profileId))
306 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
307
308
309 MediaPackageElement sourceElement = MediaPackageElementParser.getFromXml(sourceTrackAsXml);
310 if (!Track.TYPE.equals(sourceElement.getElementType()))
311 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
312
313
314 Track sourceTrack = (Track) sourceElement;
315
316 if (sourceTrack.getDuration() == null)
317 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element does not have a duration")
318 .build();
319
320 if (start < 0) {
321 start = 0;
322 } else if (duration <= 0) {
323 duration = (sourceTrack.getDuration() - start);
324 } else if (start + duration > sourceTrack.getDuration()) {
325 duration = (sourceTrack.getDuration() - start);
326 }
327
328 try {
329
330 Job job = composerService.trim(sourceTrack, profileId, start, duration);
331 return Response.ok().entity(new JaxbJob(job)).build();
332 } catch (EncoderException e) {
333 logger.warn("Unable to trim the track: " + e.getMessage());
334 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
335 }
336 }
337
338
339
340
341
342
343
344
345
346
347
348
349
350 @POST
351 @Path("mux")
352 @Produces(MediaType.TEXT_XML)
353 @RestQuery(name = "mux", description = "Starts an encoding process, which will mux the two tracks using the given encoding profile", restParameters = {
354 @RestParameter(description = "The track containing the audio stream", isRequired = false, name = "sourceAudioTrack", type = Type.TEXT, defaultValue = AUDIO_TRACK_DEFAULT),
355 @RestParameter(description = "The track containing the video stream", isRequired = false, name = "sourceVideoTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
356 @RestParameter(description = "The track containing the video stream", isRequired = false, name = "sourceTracks", type = Type.TEXT),
357 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "mp4-medium.http") }, responses = {
358 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK),
359 @RestResponse(description = "If required parameters aren't set or if the source tracks aren't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
360 public Response mux(@FormParam("audioSourceTrack") String audioSourceTrackXml,
361 @FormParam("videoSourceTrack") String videoSourceTrackXml,
362 @FormParam("sourceTracks") String sourceTracksXml, @FormParam("profileId") String profileId)
363 throws Exception {
364
365 if (StringUtils.isBlank(profileId)) {
366 return Response.status(Response.Status.BAD_REQUEST).entity("profileId must not be null").build();
367 }
368 if ((StringUtils.isBlank(audioSourceTrackXml) || StringUtils.isBlank(videoSourceTrackXml))
369 && StringUtils.isBlank(sourceTracksXml)) {
370 return Response.status(Response.Status.BAD_REQUEST)
371 .entity("audioSourceTrack, videoSourceTrack or sourceTracks must not be null").build();
372 }
373
374 try {
375 Job job;
376 Map<String, Track> sourceTracks = new HashMap<>();
377 if (StringUtils.isNotBlank(sourceTracksXml)) {
378 for (String sourceTrackEntry : StringUtils.splitByWholeSeparator(sourceTracksXml, "#|#")) {
379 String[] sourceTrackEntryTuple = StringUtils.splitByWholeSeparator(sourceTrackEntry, "#=#", 2);
380 if (sourceTrackEntryTuple.length != 2) {
381 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTracks value invalid").build();
382 }
383 sourceTracks.put(sourceTrackEntryTuple[0],
384 (Track) MediaPackageElementParser.getFromXml(sourceTrackEntryTuple[1]));
385 }
386
387 job = composerService.mux(sourceTracks, profileId);
388 } else {
389
390 MediaPackageElement audioSourceTrack = MediaPackageElementParser.getFromXml(audioSourceTrackXml);
391 if (!Track.TYPE.equals(audioSourceTrack.getElementType()))
392 return Response.status(Response.Status.BAD_REQUEST).entity("audioSourceTrack must be of type track").build();
393
394
395 MediaPackageElement videoSourceTrack = MediaPackageElementParser.getFromXml(videoSourceTrackXml);
396 if (!Track.TYPE.equals(videoSourceTrack.getElementType()))
397 return Response.status(Response.Status.BAD_REQUEST).entity("videoSourceTrack must be of type track").build();
398
399 job = composerService.mux((Track) videoSourceTrack, (Track) audioSourceTrack, profileId);
400 }
401 return Response.ok().entity(new JaxbJob(job)).build();
402 } catch (EncoderException e) {
403 logger.warn("Unable to mux tracks: " + e.getMessage());
404 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
405 }
406 }
407
408
409
410
411
412
413
414
415
416
417
418
419
420 @POST
421 @Path("image")
422 @Produces(MediaType.TEXT_XML)
423 @RestQuery(name = "image", description = "Starts an image extraction process, based on the specified encoding profile ID and the source track", restParameters = {
424 @RestParameter(description = "The track containing the video stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
425 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "player-preview.http"),
426 @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),
427 @RestParameter(description = "An optional set of key=value\\n properties", isRequired = false, name = "properties", type = TEXT) }, responses = {
428 @RestResponse(description = "Results in an xml document containing the image attachment", responseCode = HttpServletResponse.SC_OK),
429 @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")
430 public Response image(@FormParam("sourceTrack") String sourceTrackXml, @FormParam("profileId") String profileId,
431 @FormParam("time") String times, @FormParam("properties") LocalHashMap localMap) throws Exception {
432
433 if (StringUtils.isBlank(sourceTrackXml) || StringUtils.isBlank(profileId))
434 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
435
436
437 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackXml);
438 if (!Track.TYPE.equals(sourceTrack.getElementType()))
439 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
440
441 boolean timeBased = false;
442 double[] timeArray = null;
443 if (StringUtils.isNotBlank(times)) {
444
445 try {
446 timeArray = parseTimeArray(times);
447 } catch (Exception e) {
448 return Response.status(Response.Status.BAD_REQUEST).entity("could not parse times: invalid format").build();
449 }
450 timeBased = true;
451 } else if (localMap == null) {
452 return Response.status(Response.Status.BAD_REQUEST).build();
453 }
454
455 try {
456
457 Job job;
458 if (timeBased) {
459 job = composerService.image((Track) sourceTrack, profileId, timeArray);
460 } else {
461 job = composerService.image((Track) sourceTrack, profileId, localMap.getMap());
462 }
463 return Response.ok().entity(new JaxbJob(job)).build();
464 } catch (EncoderException e) {
465 logger.warn("Unable to extract image(s): " + e.getMessage());
466 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
467 }
468 }
469
470
471
472
473
474
475
476
477
478
479
480
481
482 @POST
483 @Path("imagesync")
484 @Produces(MediaType.TEXT_XML)
485 @RestQuery(name = "imagesync", description = "Synchronously extracts an image, based on the specified encoding profile ID and the source track", restParameters = {
486 @RestParameter(description = "The track containing the video stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
487 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "player-preview.http"),
488 @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)}, responses = {
489 @RestResponse(description = "Results in an xml document containing the image attachment", responseCode = HttpServletResponse.SC_OK),
490 @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "The extracted image")
491 public Response imageSync(@FormParam("sourceTrack") String sourceTrackXml, @FormParam("profileId") String profileId,
492 @FormParam("time") String times) throws Exception {
493
494 if (StringUtils.isBlank(sourceTrackXml) || StringUtils.isBlank(profileId) || StringUtils.isBlank(times)) {
495 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack, times, and profileId must not be null").build();
496 }
497
498
499 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackXml);
500 if (!Track.TYPE.equals(sourceTrack.getElementType())) {
501 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
502 }
503
504 double[] timeArray = null;
505
506 try {
507 timeArray = parseTimeArray(times);
508 } catch (Exception e) {
509 return Response.status(Response.Status.BAD_REQUEST).entity("could not parse times: invalid format").build();
510 }
511
512 try {
513 List<Attachment> result = composerService.imageSync((Track) sourceTrack, profileId, timeArray);
514 return Response.ok().entity(MediaPackageElementParser.getArrayAsXml(result)).build();
515 } catch (EncoderException e) {
516 logger.warn("Unable to extract image(s): " + e.getMessage());
517 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
518 }
519 }
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545 @POST
546 @Path("composite")
547 @Produces(MediaType.TEXT_XML)
548 @RestQuery(name = "composite", description = "Starts a video compositing process, based on the specified resolution, encoding profile ID, the source elements and their layouts", restParameters = {
549 @RestParameter(description = "The resolution size of the resulting video as JSON", isRequired = true, name = "compositeSize", type = Type.STRING),
550 @RestParameter(description = "The lower source track containing the lower video", isRequired = true, name = "lowerTrack", type = Type.TEXT),
551 @RestParameter(description = "The lower layout containing the JSON definition of the layout", isRequired = true, name = "lowerLayout", type = Type.TEXT),
552 @RestParameter(description = "The upper source track containing the upper video", isRequired = false, name = "upperTrack", type = Type.TEXT),
553 @RestParameter(description = "The upper layout containing the JSON definition of the layout", isRequired = false, name = "upperLayout", type = Type.TEXT),
554 @RestParameter(description = "The watermark source attachment containing watermark image", isRequired = false, name = "watermarkTrack", type = Type.TEXT),
555 @RestParameter(description = "The watermark layout containing the JSON definition of the layout", isRequired = false, name = "watermarkLayout", type = Type.TEXT),
556 @RestParameter(description = "The background color", isRequired = false, name = "background", type = Type.TEXT, defaultValue = "black"),
557 @RestParameter(description = "The name of the audio source (lower or upper or both)", isRequired = false, name = "audioSourceName", type = Type.TEXT, defaultValue = ComposerService.BOTH),
558 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING) }, responses = {
559 @RestResponse(description = "Results in an xml document containing the compound video track", responseCode = HttpServletResponse.SC_OK),
560 @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 = "")
561 public Response composite(@FormParam("compositeSize") String compositeSizeJson,
562 @FormParam("lowerTrack") String lowerTrackXml, @FormParam("lowerLayout") String lowerLayoutJson,
563 @FormParam("upperTrack") String upperTrackXml, @FormParam("upperLayout") String upperLayoutJson,
564 @FormParam("watermarkAttachment") String watermarkAttachmentXml,
565 @FormParam("watermarkLayout") String watermarkLayoutJson, @FormParam("profileId") String profileId,
566 @FormParam("background") @DefaultValue("black") String background,
567 @FormParam("sourceAudioName") @DefaultValue(ComposerService.BOTH) String sourceAudioName) throws Exception {
568
569 if (StringUtils.isBlank(compositeSizeJson) || StringUtils.isBlank(lowerTrackXml)
570 || StringUtils.isBlank(lowerLayoutJson) || StringUtils.isBlank(profileId))
571 return Response.status(Response.Status.BAD_REQUEST).entity("One of the required parameters must not be null")
572 .build();
573
574
575 MediaPackageElement lowerTrack = MediaPackageElementParser.getFromXml(lowerTrackXml);
576 Layout lowerLayout = Serializer.layout(JsonObj.jsonObj(lowerLayoutJson));
577 if (!Track.TYPE.equals(lowerTrack.getElementType()))
578 return Response.status(Response.Status.BAD_REQUEST).entity("lowerTrack element must be of type track").build();
579 LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>((Track) lowerTrack, lowerLayout);
580
581 Option<LaidOutElement<Track>> upperLaidOutElement = Option.<LaidOutElement<Track>> none();
582 if (StringUtils.isNotBlank(upperTrackXml)) {
583 MediaPackageElement upperTrack = MediaPackageElementParser.getFromXml(upperTrackXml);
584 Layout upperLayout = Serializer.layout(JsonObj.jsonObj(upperLayoutJson));
585 if (!Track.TYPE.equals(upperTrack.getElementType())) {
586 return Response.status(Response.Status.BAD_REQUEST).entity("upperTrack element must be of type track").build();
587 }
588 upperLaidOutElement = Option.option(new LaidOutElement<Track>((Track) upperTrack, upperLayout));
589 }
590 Option<LaidOutElement<Attachment>> watermarkLaidOutElement = Option.<LaidOutElement<Attachment>> none();
591 if (StringUtils.isNotBlank(watermarkAttachmentXml)) {
592 Layout watermarkLayout = Serializer.layout(JsonObj.jsonObj(watermarkLayoutJson));
593 MediaPackageElement watermarkAttachment = MediaPackageElementParser.getFromXml(watermarkAttachmentXml);
594 if (!Attachment.TYPE.equals(watermarkAttachment.getElementType()))
595 return Response.status(Response.Status.BAD_REQUEST).entity("watermarkTrack element must be of type track")
596 .build();
597 watermarkLaidOutElement = Option.some(new LaidOutElement<Attachment>((Attachment) watermarkAttachment,
598 watermarkLayout));
599 }
600
601 Dimension compositeTrackSize = Serializer.dimension(JsonObj.jsonObj(compositeSizeJson));
602
603 try {
604
605 Job job = composerService.composite(compositeTrackSize, upperLaidOutElement, lowerLaidOutElement,
606 watermarkLaidOutElement, profileId, background, sourceAudioName);
607 return Response.ok().entity(new JaxbJob(job)).build();
608 } catch (EncoderException e) {
609 logger.warn("Unable to composite video: " + e.getMessage());
610 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
611 }
612 }
613
614
615
616
617
618
619
620
621
622
623
624
625
626 @POST
627 @Path("concat")
628 @Produces(MediaType.TEXT_XML)
629 @RestQuery(name = "concat", description = "Starts a video concating process from multiple videos, based on the specified encoding profile ID and the source tracks", restParameters = {
630 @RestParameter(description = "The source tracks to concat as XML", isRequired = true, name = "sourceTracks", type = Type.TEXT),
631 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING),
632 @RestParameter(description = "The resolution dimension of the concat video as JSON", isRequired = false, name = "outputDimension", type = Type.STRING),
633 @RestParameter(description = "The frame rate of the concat video (should be positive, e.g. 25.0). Negative values and zero will cause no FFmpeg fps filter to be used in the filter chain.",
634 isRequired = false, name = "outputFrameRate", type = Type.STRING),
635 @RestParameter(description = "The source files have the same codecs and should not be re-encoded", isRequired = false, name = "sameCodec",type = Type.TEXT, defaultValue = "false")}, responses = {
636 @RestResponse(description = "Results in an xml document containing the video track", responseCode = HttpServletResponse.SC_OK),
637 @RestResponse(description = "If required parameters aren't set or if sourceTracks aren't from the type Track or not at least two tracks are present",
638 responseCode = HttpServletResponse.SC_BAD_REQUEST)}, returnDescription = "")
639 public Response concat(@FormParam("sourceTracks") String sourceTracksXml, @FormParam("profileId") String profileId,
640 @FormParam("outputDimension") String outputDimension, @FormParam("outputFrameRate") String outputFrameRate,
641 @FormParam("sameCodec") String sameCodec) throws Exception {
642
643 if (StringUtils.isBlank(sourceTracksXml) || StringUtils.isBlank(profileId))
644 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTracks and profileId must not be null").build();
645
646
647 List<? extends MediaPackageElement> tracks = MediaPackageElementParser.getArrayFromXml(sourceTracksXml);
648 if (tracks.size() < 2)
649 return Response.status(Response.Status.BAD_REQUEST).entity("At least two tracks must be set to concat").build();
650
651 for (MediaPackageElement elem : tracks) {
652 if (!Track.TYPE.equals(elem.getElementType()))
653 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTracks must be of type track").build();
654 }
655 float fps = NumberUtils.toFloat(outputFrameRate, -1.0f);
656 try {
657
658 Dimension dimension = null;
659 if (StringUtils.isNotBlank(outputDimension)) {
660 dimension = Serializer.dimension(JsonObj.jsonObj(outputDimension));
661 }
662 boolean hasSameCodec = Boolean.parseBoolean(sameCodec);
663 Job job = null;
664 if (fps > 0) {
665 job = composerService.concat(profileId, dimension, fps, hasSameCodec, tracks.toArray(new Track[tracks.size()]));
666 } else {
667 job = composerService.concat(profileId, dimension, hasSameCodec, tracks.toArray(new Track[tracks.size()]));
668 }
669 return Response.ok().entity(new JaxbJob(job)).build();
670 } catch (EncoderException e) {
671 logger.warn("Unable to concat videos: " + e.getMessage());
672 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
673 }
674 }
675
676
677
678
679
680
681
682
683
684
685
686
687
688 @POST
689 @Path("imagetovideo")
690 @Produces(MediaType.TEXT_XML)
691 @RestQuery(name = "imagetovideo", description = "Starts an image converting process to a video, based on the specified encoding profile ID and the source image attachment", restParameters = {
692 @RestParameter(description = "The resulting video time in seconds", isRequired = false, name = "time", type = Type.STRING, defaultValue = "1"),
693 @RestParameter(description = "The attachment containing the image to convert", isRequired = true, name = "sourceAttachment", type = Type.TEXT),
694 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING) }, responses = {
695 @RestResponse(description = "Results in an xml document containing the video track", responseCode = HttpServletResponse.SC_OK),
696 @RestResponse(description = "If required parameters aren't set or if sourceAttachment isn't from the type Attachment", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
697 public Response imageToVideo(@FormParam("sourceAttachment") String sourceAttachmentXml,
698 @FormParam("profileId") String profileId, @FormParam("time") @DefaultValue("1") String timeString)
699 throws Exception {
700
701 if (StringUtils.isBlank(sourceAttachmentXml) || StringUtils.isBlank(profileId))
702 return Response.status(Response.Status.BAD_REQUEST).entity("sourceAttachment and profileId must not be null")
703 .build();
704
705
706 Double time;
707 try {
708 time = Double.parseDouble(timeString);
709 } catch (Exception e) {
710 logger.info("Unable to parse time {} as long value!", timeString);
711 return Response.status(Response.Status.BAD_REQUEST).entity("Could not parse time: invalid format").build();
712 }
713
714
715 MediaPackageElement sourceAttachment = MediaPackageElementParser.getFromXml(sourceAttachmentXml);
716 if (!Attachment.TYPE.equals(sourceAttachment.getElementType()))
717 return Response.status(Response.Status.BAD_REQUEST).entity("sourceAttachment element must be of type attachment")
718 .build();
719
720 try {
721
722 Job job = composerService.imageToVideo((Attachment) sourceAttachment, profileId, time);
723 return Response.ok().entity(new JaxbJob(job)).build();
724 } catch (EncoderException e) {
725 logger.warn("Unable to convert image to video: " + e.getMessage());
726 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
727 }
728 }
729
730
731
732
733
734
735
736
737
738
739
740 @POST
741 @Path("convertimage")
742 @Produces(MediaType.TEXT_XML)
743 @RestQuery(name = "convertimage", description = "Starts an image conversion process, based on the specified encoding profile ID and the source image", restParameters = {
744 @RestParameter(description = "The original image", isRequired = true, name = "sourceImage", type = Type.TEXT, defaultValue = IMAGE_ATTACHMENT_DEFAULT),
745 @RestParameter(description = "A comma separated list of encoding profiles to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "image-conversion.http") }, responses = {
746 @RestResponse(description = "Results in an xml document containing the image attachment", responseCode = HttpServletResponse.SC_OK),
747 @RestResponse(description = "If required parameters aren't set or if sourceImage isn't from the type Attachment", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
748 public Response convertImage(@FormParam("sourceImage") String sourceImageXml, @FormParam("profileId") String profileId)
749 throws Exception {
750
751 if (StringUtils.isBlank(sourceImageXml) || StringUtils.isBlank(profileId))
752 return Response.status(Response.Status.BAD_REQUEST).entity("sourceImage and profileId must not be null").build();
753
754
755 MediaPackageElement sourceImage = MediaPackageElementParser.getFromXml(sourceImageXml);
756 if (!Attachment.TYPE.equals(sourceImage.getElementType()))
757 return Response.status(Response.Status.BAD_REQUEST).entity("sourceImage element must be of type track").build();
758
759 try {
760
761 Job job = composerService.convertImage((Attachment) sourceImage, StringUtils.split(profileId, ','));
762 return Response.ok().entity(new JaxbJob(job)).build();
763 } catch (EncoderException e) {
764 logger.warn("Unable to convert image: " + e.getMessage());
765 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
766 }
767 }
768
769
770
771
772
773
774
775
776
777
778
779
780 @POST
781 @Path("demux")
782 @Produces(MediaType.TEXT_XML)
783 @RestQuery(name = "demux", description = "Starts an demux process that produces multiple outputs, based on the specified encoding profile ID and the track", restParameters = {
784 @RestParameter(description = "The track containing the stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
785 @RestParameter(description = "The encoding profile to use", isRequired = true, name = "profileId", type = Type.STRING, defaultValue = "demux.work") }, responses = {
786 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK),
787 @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
788 public Response demux(@FormParam("sourceTrack") String sourceTrackAsXml, @FormParam("profileId") String profileId)
789 throws Exception {
790
791 if (StringUtils.isBlank(sourceTrackAsXml) || StringUtils.isBlank(profileId))
792 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileId must not be null").build();
793
794
795 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackAsXml);
796 if (!Track.TYPE.equals(sourceTrack.getElementType()))
797 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
798
799 try {
800
801 Job job = composerService.demux((Track) sourceTrack, profileId);
802 return Response.ok().entity(new JaxbJob(job)).build();
803 } catch (EncoderException e) {
804 logger.warn("Unable to encode the track: " + e);
805 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
806 }
807 }
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824 @POST
825 @Path("processsmil")
826 @Produces(MediaType.TEXT_XML)
827 @RestQuery(name = "processsmil", description = "Starts an encoding process, based on the tracks and edit points in the smil and specified encoding profile IDs", restParameters = {
828 @RestParameter(description = "The smil containing the tracks and edit points", isRequired = true, name = "smilAsXml", type = Type.TEXT),
829 @RestParameter(description = "The id (paramgroup) of the track to encode", isRequired = false, name = "trackId", type = Type.STRING, defaultValue = ""),
830 @RestParameter(description = "MediaType - v for video only, a for audio only, audiovisual otherwise", isRequired = false, name = "mediaType", type = Type.STRING, defaultValue = "o"),
831 @RestParameter(description = "The encoding profiles to use", isRequired = true, name = "profileIds", type = Type.STRING) }, responses = {
832 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK),
833 @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
834 public Response processSmil(@FormParam("smilAsXml") String smilAsXml, @FormParam("trackId") String trackId,
835 @FormParam("mediaType") String mediaType, @FormParam("profileIds") String profileIds) throws Exception {
836
837 if (StringUtils.isBlank(smilAsXml) || StringUtils.isBlank(profileIds))
838 return Response.status(Response.Status.BAD_REQUEST).entity("smil and profileId must not be null").build();
839
840
841 String[] profiles = StringUtils.split(profileIds, ",");
842 Smil smil;
843 try {
844 smil = smilService.fromXml(smilAsXml).getSmil();
845 } catch (Exception e) {
846 return Response.status(Response.Status.BAD_REQUEST).entity("smil must be readable").build();
847 }
848
849 try {
850
851 Job job = composerService.processSmil(smil, trackId, mediaType, Arrays.asList(profiles));
852 return Response.ok().entity(new JaxbJob(job)).build();
853 } catch (EncoderException e) {
854 logger.warn("Unable to process the smil: " + e);
855 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
856 }
857 }
858
859 @POST
860 @Path("multiencode")
861 @Produces(MediaType.TEXT_XML)
862 @RestQuery(name = "multiencode", description = "Starts an encoding process that produces multiple outputs, based on the specified encoding profile ID and the track",
863 restParameters = {
864 @RestParameter(description = "The track containing the stream", isRequired = true, name = "sourceTrack", type = Type.TEXT, defaultValue = VIDEO_TRACK_DEFAULT),
865 @RestParameter(description = "The comma-delimited encoding profiles to use", isRequired = true, name = "profileIds", type = Type.STRING, defaultValue = "mp4-medium.http,mp4-low.http")
866 }, responses = {
867 @RestResponse(description = "Results in an xml document containing the job for the encoding task", responseCode = HttpServletResponse.SC_OK),
868 @RestResponse(description = "If required parameters aren't set or if sourceTrack isn't from the type Track", responseCode = HttpServletResponse.SC_BAD_REQUEST)
869 }, returnDescription = "")
870 public Response multiEncode(@FormParam("sourceTrack") String sourceTrackAsXml,
871 @FormParam("profileIds") String profileIds) throws Exception {
872
873 if (StringUtils.isBlank(sourceTrackAsXml) || StringUtils.isBlank(profileIds))
874 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack and profileIds must not be null").build();
875
876
877 MediaPackageElement sourceTrack = MediaPackageElementParser.getFromXml(sourceTrackAsXml);
878 if (!Track.TYPE.equals(sourceTrack.getElementType()))
879 return Response.status(Response.Status.BAD_REQUEST).entity("sourceTrack element must be of type track").build();
880
881 try {
882
883 String[] profiles = StringUtils.split(profileIds, ",");
884 Job job = composerService.multiEncode((Track) sourceTrack, Arrays.asList(profiles));
885 return Response.ok().entity(new JaxbJob(job)).build();
886 } catch (EncoderException e) {
887 logger.warn("Unable to encode the track: ", e);
888 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
889 }
890 }
891
892
893
894
895
896
897
898
899
900
901
902 @POST
903 @Path("convertimagesync")
904 @Produces(MediaType.TEXT_XML)
905 @RestQuery(name = "convertimagesync", description = "Synchronously converts an image, based on the specified encoding profiles and the source image", restParameters = {
906 @RestParameter(description = "The original image", isRequired = true, name = "sourceImage", type = Type.TEXT, defaultValue = IMAGE_ATTACHMENT_DEFAULT),
907 @RestParameter(description = "The encoding profiles to use", isRequired = true, name = "profileIds", type = Type.STRING, defaultValue = "image-conversion.http") }, responses = {
908 @RestResponse(description = "Results in an xml document containing the image attachments", responseCode = HttpServletResponse.SC_OK),
909 @RestResponse(description = "If required parameters aren't set or if sourceImage isn't from the type attachment", responseCode = HttpServletResponse.SC_BAD_REQUEST) }, returnDescription = "")
910 public Response convertImageSync(@FormParam("sourceImage") String sourceImageXml, @FormParam("profileIds")
911 String profileIds) throws Exception {
912
913 if (StringUtils.isBlank(sourceImageXml) || StringUtils.isBlank(profileIds))
914 return Response.status(Response.Status.BAD_REQUEST).entity("sourceImage and profileIds must not be null").build();
915
916
917 MediaPackageElement sourceImage = MediaPackageElementParser.getFromXml(sourceImageXml);
918 if (!Attachment.TYPE.equals(sourceImage.getElementType()))
919 return Response.status(Response.Status.BAD_REQUEST).entity("sourceImage element must be of type track").build();
920
921 try {
922 List<Attachment> results = composerService.convertImageSync((Attachment) sourceImage,
923 StringUtils.split(profileIds, ','));
924 return Response.ok().entity(MediaPackageElementParser.getArrayAsXml(results)).build();
925 } catch (EncoderException e) {
926 logger.warn("Unable to convert image: " + e.getMessage());
927 return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
928 }
929 }
930
931 @GET
932 @Path("profiles.xml")
933 @Produces(MediaType.TEXT_XML)
934 @RestQuery(name = "profiles", description = "Retrieve the encoding profiles", responses = { @RestResponse(description = "Results in an xml document describing the available encoding profiles", responseCode = HttpServletResponse.SC_OK) }, returnDescription = "")
935 public EncodingProfileList listProfiles() {
936 List<EncodingProfileImpl> list = new ArrayList<EncodingProfileImpl>();
937 for (EncodingProfile p : composerService.listProfiles()) {
938 list.add((EncodingProfileImpl) p);
939 }
940 return new EncodingProfileList(list);
941 }
942
943 @GET
944 @Path("profile/{id}.xml")
945 @Produces(MediaType.TEXT_XML)
946 @RestQuery(name = "profilesID", description = "Retrieve an encoding profile", pathParameters = { @RestParameter(name = "id", description = "the profile ID", isRequired = false, type = RestParameter.Type.STRING) }, responses = {
947 @RestResponse(description = "Results in an xml document describing the requested encoding profile", responseCode = HttpServletResponse.SC_OK),
948 @RestResponse(description = "If profile has not been found", responseCode = HttpServletResponse.SC_NOT_FOUND) }, returnDescription = "")
949 public Response getProfile(@PathParam("id") String profileId) throws NotFoundException {
950 EncodingProfileImpl profile = (EncodingProfileImpl) composerService.getProfile(profileId);
951 if (profile == null)
952 throw new NotFoundException();
953 return Response.ok(profile).build();
954 }
955
956
957
958
959
960
961 @Override
962 public JobProducer getService() {
963 if (composerService instanceof JobProducer)
964 return (JobProducer) composerService;
965 else
966 return null;
967 }
968
969
970
971
972
973
974 @Override
975 public ServiceRegistry getServiceRegistry() {
976 return serviceRegistry;
977 }
978
979
980
981
982
983
984
985
986 protected double[] parseTimeArray(String times) {
987 String[] timeStringArray = times.split(";");
988 List<Double> parsedTimeArray = new LinkedList<Double>();
989 for (String timeString : timeStringArray) {
990 String trimmed = StringUtils.trim(timeString);
991 if (StringUtils.isNotBlank(trimmed)) {
992 parsedTimeArray.add(Double.parseDouble(timeString));
993 }
994 }
995 double[] timeArray = new double[parsedTimeArray.size()];
996 for (int i = 0; i < parsedTimeArray.size(); i++) {
997 timeArray[i] = parsedTimeArray.get(i);
998 }
999 return timeArray;
1000 }
1001
1002 }