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