View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * A REST endpoint delegating functionality to the {@link ComposerService}
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   /** The logger */
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   /** The base server URL */
143   protected String serverUrl;
144 
145   /** The composer service */
146   protected ComposerService composerService = null;
147 
148   /** The service registry */
149   protected ServiceRegistry serviceRegistry = null;
150 
151   /** The smil service */
152   protected SmilService smilService = null;
153 
154   @Reference
155   public void setSmilService(SmilService smilService) {
156     this.smilService = smilService;
157   }
158 
159   /**
160    * Callback from the OSGi declarative services to set the service registry.
161    *
162    * @param serviceRegistry
163    *          the service registry
164    */
165   @Reference
166   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
167     this.serviceRegistry = serviceRegistry;
168   }
169 
170   /**
171    * Sets the composer service.
172    *
173    * @param composerService
174    *          the composer service
175    */
176   @Reference
177   public void setComposerService(ComposerService composerService) {
178     this.composerService = composerService;
179   }
180 
181   /**
182    * Callback from OSGi that is called when this service is activated.
183    *
184    * @param cc
185    *          OSGi component context
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    * Encodes a track.
197    *
198    * @param sourceTrackAsXml
199    *          The source track
200    * @param profileId
201    *          The profile to use in encoding this track
202    * @return A response containing the job for this encoding job in the response body.
203    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the track
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       // Asynchronously encode the specified tracks
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    * Encodes a track to multiple tracks in parallel.
237    *
238    * @param sourceTrackAsXml
239    *          The source track
240    * @param profileId
241    *          The profile to use in encoding this track
242    * @return A response containing the job for this encoding job in the response body.
243    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the track
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     // Asynchronously encode the specified tracks
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    * Trims a track to a new length.
277    *
278    * @param sourceTrackAsXml
279    *          The source track
280    * @param profileId
281    *          the encoding profile to use for trimming
282    * @param start
283    *          the new trimming start time
284    * @param duration
285    *          the new video duration
286    * @return A response containing the job for this encoding job in the response body.
287    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the track
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     // Make sure the trim times make sense
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       // Asynchronously encode the specified tracks
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    * Encodes a track.
338    *
339    * @param audioSourceTrackXml
340    *          The audio source track
341    * @param videoSourceTrackXml
342    *          The video source track
343    * @param profileId
344    *          The profile to use in encoding this track
345    * @return A response containing the job for this encoding job in the response body.
346    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the audio track
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     // Deserialize the video track
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       // Asynchronously encode the specified tracks
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    * Encodes a track in a media package.
389    *
390    * @param sourceTrackXml
391    *          The source track
392    * @param profileId
393    *          The profile to use in encoding this track
394    * @param times
395    *          one or more times in seconds separated by comma
396    * @return A {@link Response} with the resulting track in the response body
397    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source track
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       // parse time codes
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       // Asynchronously encode the specified tracks
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    * Encodes a track in a media package.
451    *
452    * @param sourceTrackXml
453    *          The source track
454    * @param profileId
455    *          The profile to use in encoding this track
456    * @param times
457    *          one or more times in seconds separated by comma
458    * @return A {@link Response} with the resulting track in the response body
459    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source track
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     // parse time codes
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    * Compose two videos into one with an optional watermark.
502    *
503    * @param compositeSizeJson
504    *          The composite track dimension as JSON
505    * @param lowerTrackXml
506    *          The lower track of the composition as XML
507    * @param lowerLayoutJson
508    *          The lower layout as JSON
509    * @param upperTrackXml
510    *          The upper track of the composition as XML
511    * @param upperLayoutJson
512    *          The upper layout as JSON
513    * @param watermarkAttachmentXml
514    *          The watermark image attachment of the composition as XML
515    * @param watermarkLayoutJson
516    *          The watermark layout as JSON
517    * @param profileId
518    *          The encoding profile to use
519    * @param background
520    *          The background color
521    * @return A {@link Response} with the resulting track in the response body
522    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source elements
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       // Asynchronously composite the specified source elements
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    * Concat multiple tracks having the same codec to a single track.
595    *
596    * @param sourceTracksXml
597    *          an array of track to concat in order of the array as XML
598    * @param profileId
599    *          The encoding profile to use
600    * @param outputDimension
601    *          The output dimension as JSON
602    * @return A {@link Response} with the resulting track in the response body
603    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source track
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       // Asynchronously concat the specified tracks together
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    * Transforms an image attachment to a video track
657    *
658    * @param sourceAttachmentXml
659    *          The source image attachment
660    * @param profileId
661    *          The profile to use for encoding
662    * @param timeString
663    *          the length of the resulting video track in seconds
664    * @return A {@link Response} with the resulting track in the response body
665    * @throws Exception
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     // Ensure that the POST parameters are present
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     // parse time
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     // Deserialize the source track
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       // Asynchronously convert the specified attachment to a video
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    * Converts an image to another format.
711    *
712    * @param sourceImageXml
713    *          The source image
714    * @param profileId
715    *          The profile to use in image conversion
716    * @return A {@link Response} with the resulting image in the response body
717    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source track
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       // Asynchronously convert the specified image
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    * Demuxes a track into multiple outputs
750    *
751    * @param sourceTrackAsXml
752    *          The source track
753    * @param profileId
754    *          The profile to use in encoding this track
755    * @return A response containing the job for this encoding job in the response body.
756    * @throws Exception
757    *           - if it fails
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     // Ensure that the POST parameters are present
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     // Deserialize the track
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       // Asynchronously encode the specified tracks
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    * ProcessSmil - encode a video based on descriptions in a smil file into all format in the profileIds
790    *
791    * @param smilAsXml
792    *          - smil describing a list of videos and clips in them to make up one video
793    * @param trackId
794    *          - a paramGroup Id in the smil file describing a track
795    * @param mediaType
796    *          - audio only, video only or both
797    * @param profileIds
798    *          - list of encoding profile ids
799    * @return a job running the process
800    * @throws Exception
801    *           if it fails
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     // Ensure that the POST parameters are present
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     // Deserialize the data
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       // Encode the specified tracks
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     // Ensure that the POST parameters are present
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     // Deserialize the track
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       // Encode the specified track with the profiles
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    * Synchronously converts an image to another format.
873    *
874    * @param sourceImageXml
875    *          The source image
876    * @param profileIds
877    *          The encoding profiles to use in image conversion
878    * @return A {@link Response} with the resulting image in the response body
879    * @throws Exception
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     // Ensure that the POST parameters are present
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     // Deserialize the source track
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    * {@inheritDoc}
937    *
938    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getService()
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    * {@inheritDoc}
950    *
951    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getServiceRegistry()
952    */
953   @Override
954   public ServiceRegistry getServiceRegistry() {
955     return serviceRegistry;
956   }
957 
958   /**
959    * Parses string containing times in seconds separated by comma.
960    *
961    * @param times
962    *          string to be parsed
963    * @return array of times in seconds
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 }