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