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.remote;
23  
24  import org.opencastproject.composer.api.ComposerService;
25  import org.opencastproject.composer.api.EncoderException;
26  import org.opencastproject.composer.api.EncodingProfile;
27  import org.opencastproject.composer.api.EncodingProfileBuilder;
28  import org.opencastproject.composer.api.EncodingProfileImpl;
29  import org.opencastproject.composer.api.EncodingProfileList;
30  import org.opencastproject.composer.api.LaidOutElement;
31  import org.opencastproject.composer.layout.Dimension;
32  import org.opencastproject.composer.layout.Serializer;
33  import org.opencastproject.job.api.Job;
34  import org.opencastproject.job.api.JobParser;
35  import org.opencastproject.mediapackage.Attachment;
36  import org.opencastproject.mediapackage.MediaPackageElementParser;
37  import org.opencastproject.mediapackage.MediaPackageException;
38  import org.opencastproject.mediapackage.Track;
39  import org.opencastproject.security.api.TrustedHttpClient;
40  import org.opencastproject.serviceregistry.api.RemoteBase;
41  import org.opencastproject.serviceregistry.api.ServiceRegistry;
42  import org.opencastproject.smil.entity.api.Smil;
43  
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.lang3.StringUtils;
46  import org.apache.http.HttpResponse;
47  import org.apache.http.HttpStatus;
48  import org.apache.http.client.entity.UrlEncodedFormEntity;
49  import org.apache.http.client.methods.HttpGet;
50  import org.apache.http.client.methods.HttpPost;
51  import org.apache.http.message.BasicNameValuePair;
52  import org.apache.http.util.EntityUtils;
53  import org.osgi.service.component.annotations.Component;
54  import org.osgi.service.component.annotations.Reference;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import java.io.IOException;
59  import java.nio.charset.Charset;
60  import java.util.ArrayList;
61  import java.util.Arrays;
62  import java.util.List;
63  import java.util.Locale;
64  import java.util.Map;
65  import java.util.Map.Entry;
66  import java.util.Optional;
67  import java.util.stream.Collectors;
68  
69  /**
70   * Proxies a set of remote composer services for use as a JVM-local service. Remote services are selected at random.
71   */
72  @Component(
73      property = {
74          "service.description=Composer (Encoder) Remote Service Proxy"
75      },
76      immediate = true,
77      service = { ComposerService.class }
78  )
79  public class ComposerServiceRemoteImpl extends RemoteBase implements ComposerService {
80  
81    /** The logger */
82    private static final Logger logger = LoggerFactory.getLogger(ComposerServiceRemoteImpl.class);
83  
84    public ComposerServiceRemoteImpl() {
85      super(JOB_TYPE);
86    }
87  
88    /**
89     * Sets the trusted http client
90     *
91     * @param client
92     */
93    @Override
94    @Reference
95    public void setTrustedHttpClient(TrustedHttpClient client) {
96      this.client = client;
97    }
98  
99    /**
100    * Sets the remote service manager.
101    *
102    * @param remoteServiceManager
103    */
104   @Override
105   @Reference
106   public void setRemoteServiceManager(ServiceRegistry remoteServiceManager) {
107     this.remoteServiceManager = remoteServiceManager;
108   }
109 
110   /**
111    * {@inheritDoc}
112    *
113    * @see org.opencastproject.composer.api.ComposerService#encode(org.opencastproject.mediapackage.Track,
114    *      java.lang.String)
115    */
116   @Override
117   public Job encode(Track sourceTrack, String profileId) throws EncoderException {
118     HttpPost post = new HttpPost("/encode");
119     try {
120       List<BasicNameValuePair> params = new ArrayList<>();
121       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
122       params.add(new BasicNameValuePair("profileId", profileId));
123       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
124     } catch (Exception e) {
125       throw new EncoderException("Unable to assemble a remote composer request for track " + sourceTrack, e);
126     }
127     HttpResponse response = null;
128     try {
129       response = getResponse(post);
130       if (response != null) {
131         String content = EntityUtils.toString(response.getEntity());
132         Job r = JobParser.parseJob(content);
133         logger.info("Encoding job {} started on a remote composer", r.getId());
134         return r;
135       }
136     } catch (Exception e) {
137       throw new EncoderException("Unable to encode track " + sourceTrack + " using a remote composer service", e);
138     } finally {
139       closeConnection(response);
140     }
141     throw new EncoderException("Unable to encode track " + sourceTrack + " using a remote composer service");
142   }
143 
144   /**
145    * {@inheritDoc}
146    */
147   @Override
148   public Job parallelEncode(Track sourceTrack, String profileId) throws EncoderException {
149     HttpPost post = new HttpPost("/parallelencode");
150     try {
151       List<BasicNameValuePair> params = new ArrayList<>();
152       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
153       params.add(new BasicNameValuePair("profileId", profileId));
154       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
155     } catch (Exception e) {
156       throw new EncoderException("Unable to assemble a remote composer request for track " + sourceTrack, e);
157     }
158     HttpResponse response = null;
159     try {
160       response = getResponse(post);
161       if (response != null) {
162         String content = EntityUtils.toString(response.getEntity());
163         Job r = JobParser.parseJob(content);
164         logger.info("Encoding job {} started on a remote composer", r.getId());
165         return r;
166       }
167     } catch (Exception e) {
168       throw new EncoderException("Unable to encode track " + sourceTrack + " using a remote composer service", e);
169     } finally {
170       closeConnection(response);
171     }
172     throw new EncoderException("Unable to encode track " + sourceTrack + " using a remote composer service");
173   }
174 
175   /**
176    * {@inheritDoc}
177    *
178    * @see org.opencastproject.composer.api.ComposerService#trim(Track, String, long, long)
179    */
180   @Override
181   public Job trim(Track sourceTrack, String profileId, long start, long duration) throws EncoderException {
182     HttpPost post = new HttpPost("/trim");
183     try {
184       List<BasicNameValuePair> params = new ArrayList<>();
185       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
186       params.add(new BasicNameValuePair("profileId", profileId));
187       params.add(new BasicNameValuePair("start", Long.toString(start)));
188       params.add(new BasicNameValuePair("duration", Long.toString(duration)));
189       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
190     } catch (Exception e) {
191       throw new EncoderException("Unable to assemble a remote composer request for track " + sourceTrack, e);
192     }
193     HttpResponse response = null;
194     try {
195       response = getResponse(post);
196       if (response != null) {
197         String content = EntityUtils.toString(response.getEntity());
198         Job r = JobParser.parseJob(content);
199         logger.info("Trimming job {} started on a remote composer", r.getId());
200         return r;
201       }
202     } catch (Exception e) {
203       throw new EncoderException("Unable to trim track " + sourceTrack + " using a remote composer service", e);
204     } finally {
205       closeConnection(response);
206     }
207     throw new EncoderException("Unable to trim track " + sourceTrack + " using a remote composer service");
208   }
209 
210   /**
211    * {@inheritDoc}
212    *
213    * @see org.opencastproject.composer.api.ComposerService#mux(org.opencastproject.mediapackage.Track,
214    *      org.opencastproject.mediapackage.Track, java.lang.String)
215    */
216   @Override
217   public Job mux(Track sourceVideoTrack, Track sourceAudioTrack, String profileId) throws EncoderException {
218     HttpPost post = new HttpPost("/mux");
219     try {
220       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
221       params.add(new BasicNameValuePair("videoSourceTrack", MediaPackageElementParser.getAsXml(sourceVideoTrack)));
222       params.add(new BasicNameValuePair("audioSourceTrack", MediaPackageElementParser.getAsXml(sourceAudioTrack)));
223       params.add(new BasicNameValuePair("profileId", profileId));
224       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
225     } catch (Exception e) {
226       throw new EncoderException("Unable to assemble a remote composer request", e);
227     }
228     HttpResponse response = null;
229     try {
230       response = getResponse(post);
231       if (response != null) {
232         String content = EntityUtils.toString(response.getEntity());
233         Job r = JobParser.parseJob(content);
234         logger.info("Muxing job {} started on a remote composer", r.getId());
235         return r;
236       }
237     } catch (IOException e) {
238       throw new EncoderException(e);
239     } finally {
240       closeConnection(response);
241     }
242     throw new EncoderException("Unable to mux tracks " + sourceVideoTrack + " and " + sourceAudioTrack
243             + " using a remote composer");
244   }
245 
246   /**
247    * {@inheritDoc}
248    *
249    * @see org.opencastproject.composer.api.ComposerService#mux(java.util.Map, java.lang.String)
250    */
251   @Override
252   public Job mux(Map<String, Track> sourceTracks, String profileId) throws EncoderException {
253     HttpPost post = new HttpPost("/mux");
254     try {
255       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
256       List<String> sourceTracksEntries = new ArrayList<>();
257       for (Entry<String, Track> sourceTrack : sourceTracks.entrySet()) {
258         String sourceTrackXml = MediaPackageElementParser.getAsXml(sourceTrack.getValue());
259         sourceTracksEntries.add(StringUtils.join(sourceTrack.getKey(), "#=#", sourceTrackXml));
260       }
261       params.add(new BasicNameValuePair("sourceTracks", StringUtils.join(sourceTracksEntries, "#|#")));
262       params.add(new BasicNameValuePair("profileId", profileId));
263       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
264     } catch (Exception e) {
265       throw new EncoderException("Unable to assemble a remote composer request", e);
266     }
267     HttpResponse response = null;
268     try {
269       response = getResponse(post);
270       if (response != null) {
271         String content = EntityUtils.toString(response.getEntity());
272         Job r = JobParser.parseJob(content);
273         logger.info("Muxing job {} started on a remote composer", r.getId());
274         return r;
275       }
276     } catch (IOException e) {
277       throw new EncoderException(e);
278     } finally {
279       closeConnection(response);
280     }
281     throw new EncoderException("Unable to mux tracks " + sourceTracks.entrySet().stream()
282         .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue().getIdentifier())).collect(
283         Collectors.joining(", ")) + " using a remote composer");
284   }
285 
286   /**
287    * {@inheritDoc}
288    *
289    * @see org.opencastproject.composer.api.ComposerService#getProfile(java.lang.String)
290    */
291   @Override
292   public EncodingProfile getProfile(String profileId) {
293     HttpGet get = new HttpGet("/profile/" + profileId + ".xml");
294     HttpResponse response = null;
295     try {
296       response = getResponse(get, HttpStatus.SC_OK, HttpStatus.SC_NOT_FOUND);
297       if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
298         return EncodingProfileBuilder.getInstance().parseProfile(response.getEntity().getContent());
299       } else {
300         return null;
301       }
302     } catch (Exception e) {
303       throw new RuntimeException(e);
304     } finally {
305       closeConnection(response);
306     }
307   }
308 
309   /**
310    * {@inheritDoc}
311    */
312   @Override
313   public Job image(Track sourceTrack, String profileId, double... times) throws EncoderException {
314     HttpPost post = new HttpPost("/image");
315     try {
316       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
317       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
318       params.add(new BasicNameValuePair("profileId", profileId));
319       params.add(new BasicNameValuePair("time", buildTimeArray(times)));
320       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
321     } catch (Exception e) {
322       throw new EncoderException(e);
323     }
324     HttpResponse response = null;
325     try {
326       response = getResponse(post);
327       if (response != null) {
328         Job r = JobParser.parseJob(response.getEntity().getContent());
329         logger.info("Image extraction job {} started on a remote composer", r.getId());
330         return r;
331       }
332     } catch (Exception e) {
333       throw new EncoderException(e);
334     } finally {
335       closeConnection(response);
336     }
337     throw new EncoderException("Unable to compose an image from track " + sourceTrack
338             + " using the remote composer service proxy");
339   }
340 
341   @Override
342   public List<Attachment> imageSync(Track sourceTrack, String profileId, double... times)
343           throws EncoderException, MediaPackageException {
344     HttpPost post = new HttpPost("/imagesync");
345     try {
346       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
347       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
348       params.add(new BasicNameValuePair("profileId", profileId));
349       params.add(new BasicNameValuePair("time", buildTimeArray(times)));
350       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
351     } catch (Exception e) {
352       throw new EncoderException(e);
353     }
354     HttpResponse response = null;
355     try {
356       response = getResponse(post);
357       if (response != null) {
358         final String xml = IOUtils.toString(response.getEntity().getContent(), Charset.forName("utf-8"));
359         return MediaPackageElementParser.getArrayFromXml(xml)
360             .stream().map(e -> (Attachment)e)
361             .collect(Collectors.toList());
362       }
363     } catch (Exception e) {
364       throw new EncoderException(e);
365     } finally {
366       closeConnection(response);
367     }
368     throw new EncoderException("Unable to compose an image from track " + sourceTrack
369         + " using the remote composer service proxy");
370   }
371 
372   /**
373    * {@inheritDoc}
374    *
375    * @see org.opencastproject.composer.api.ComposerService#image(Track, String, Map)
376    */
377   @Override
378   public Job image(Track sourceTrack, String profileId, Map<String, String> properties) throws EncoderException,
379           MediaPackageException {
380     HttpPost post = new HttpPost("/image");
381     try {
382       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
383       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
384       params.add(new BasicNameValuePair("profileId", profileId));
385       if (properties != null) {
386         params.add(new BasicNameValuePair("properties", mapToString(properties)));
387       }
388       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
389     } catch (Exception e) {
390       throw new EncoderException(e);
391     }
392     HttpResponse response = null;
393     try {
394       response = getResponse(post);
395       if (response != null) {
396         Job r = JobParser.parseJob(response.getEntity().getContent());
397         logger.info("Image extraction job {} started on a remote composer", r.getId());
398         return r;
399       }
400     } catch (Exception e) {
401       throw new EncoderException(e);
402     } finally {
403       closeConnection(response);
404     }
405     throw new EncoderException("Unable to compose an image from track " + sourceTrack
406             + " using the remote composer service proxy");
407   }
408 
409   /**
410    * {@inheritDoc}
411    *
412    * @see org.opencastproject.composer.api.ComposerService#convertImage(org.opencastproject.mediapackage.Attachment,
413    *      java.lang.String...)
414    */
415   @Override
416   public Job convertImage(Attachment image, String... profileIds) throws EncoderException, MediaPackageException {
417     HttpPost post = new HttpPost("/convertimage");
418     try {
419       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
420       params.add(new BasicNameValuePair("sourceImage", MediaPackageElementParser.getAsXml(image)));
421       params.add(new BasicNameValuePair("profileId", StringUtils.join(profileIds, ',')));
422       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
423     } catch (Exception e) {
424       throw new EncoderException(e);
425     }
426     HttpResponse response = null;
427     try {
428       response = getResponse(post);
429       if (response != null) {
430         Job r = JobParser.parseJob(response.getEntity().getContent());
431         logger.info("Image conversion job {} started on a remote composer", r.getId());
432         return r;
433       }
434     } catch (Exception e) {
435       throw new EncoderException(e);
436     } finally {
437       closeConnection(response);
438     }
439     throw new EncoderException("Unable to convert image at " + image + " using the remote composer service proxy");
440   }
441 
442   /**
443    * {@inheritDoc}
444    *
445    * @see org.opencastproject.composer.api.ComposerService#convertImageSync(
446    *      org.opencastproject.mediapackage.Attachment, java.lang.String...)
447    */
448   @Override
449   public List<Attachment> convertImageSync(Attachment image, String... profileIds)
450           throws EncoderException, MediaPackageException {
451     HttpPost post = new HttpPost("/convertimagesync");
452     try {
453       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
454       params.add(new BasicNameValuePair("sourceImage", MediaPackageElementParser.getAsXml(image)));
455       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ',')));
456       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
457     } catch (Exception e) {
458       throw new EncoderException(e);
459     }
460     HttpResponse response = null;
461     try {
462       response = getResponse(post);
463       if (response != null) {
464         final String xml = IOUtils.toString(response.getEntity().getContent(), Charset.forName("utf-8"));
465         return MediaPackageElementParser.getArrayFromXml(xml).stream()
466             .map(a -> (Attachment) a)
467             .collect(Collectors.toList());
468       }
469     } catch (Exception e) {
470       throw new EncoderException(e);
471     } finally {
472       closeConnection(response);
473     }
474     throw new EncoderException("Unable to convert image at " + image + " using the remote composer service proxy");
475   }
476 
477   /**
478    * {@inheritDoc}
479    *
480    * @see org.opencastproject.composer.api.ComposerService#listProfiles()
481    */
482   @Override
483   public EncodingProfile[] listProfiles() {
484     HttpGet get = new HttpGet("/profiles.xml");
485     HttpResponse response = null;
486     try {
487       response = getResponse(get);
488       if (response != null) {
489         EncodingProfileList profileList = EncodingProfileBuilder.getInstance().parseProfileList(
490                 response.getEntity().getContent());
491         List<EncodingProfileImpl> list = profileList.getProfiles();
492         return list.toArray(new EncodingProfile[list.size()]);
493       }
494     } catch (Exception e) {
495       throw new RuntimeException(
496               "Unable to list the encoding profiles registered with the remote composer service proxy", e);
497     } finally {
498       closeConnection(response);
499     }
500     throw new RuntimeException("Unable to list the encoding profiles registered with the remote composer service "
501         + "proxy");
502   }
503 
504   /**
505    * Builds string containing times in seconds separated by comma.
506    *
507    * @param times
508    *          time array to be converted to string
509    * @return string represented specified time array
510    */
511   protected String buildTimeArray(double[] times) {
512     if (times.length == 0) {
513       return "";
514     }
515 
516     StringBuilder builder = new StringBuilder();
517     builder.append(Double.toString(times[0]));
518     for (int i = 1; i < times.length; i++) {
519       builder.append(";" + Double.toString(times[i]));
520     }
521     return builder.toString();
522   }
523 
524   @Override
525   public Job composite(Dimension compositeTrackSize, Optional<LaidOutElement<Track>> upperTrack,
526           LaidOutElement<Track> lowerTrack, Optional<LaidOutElement<Attachment>> watermark, String profileId,
527           String background, String sourceAudioName) throws EncoderException, MediaPackageException {
528     HttpPost post = new HttpPost("/composite");
529     try {
530       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
531       params.add(new BasicNameValuePair("compositeSize", Serializer.json(compositeTrackSize).toJson()));
532       params.add(new BasicNameValuePair("lowerTrack", MediaPackageElementParser.getAsXml(lowerTrack.getElement())));
533       params.add(new BasicNameValuePair("lowerLayout", Serializer.json(lowerTrack.getLayout()).toJson()));
534       if (upperTrack.isPresent()) {
535         params.add(new BasicNameValuePair("upperTrack", MediaPackageElementParser.getAsXml(upperTrack.get()
536                 .getElement())));
537         params.add(new BasicNameValuePair("upperLayout", Serializer.json(upperTrack.get().getLayout()).toJson()));
538       }
539 
540       if (watermark.isPresent()) {
541         params.add(new BasicNameValuePair("watermarkAttachment", MediaPackageElementParser.getAsXml(watermark.get()
542                 .getElement())));
543         params.add(new BasicNameValuePair("watermarkLayout", Serializer.json(watermark.get().getLayout()).toJson()));
544       }
545       params.add(new BasicNameValuePair("profileId", profileId));
546       params.add(new BasicNameValuePair("background", background));
547       params.add(new BasicNameValuePair("sourceAudioName", sourceAudioName));
548       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
549     } catch (Exception e) {
550       throw new EncoderException(e);
551     }
552     HttpResponse response = null;
553     try {
554       response = getResponse(post);
555       if (response != null) {
556         Job r = JobParser.parseJob(response.getEntity().getContent());
557         logger.info("Composite video job {} started on a remote composer", r.getId());
558         return r;
559       }
560     } catch (Exception e) {
561       throw new EncoderException(e);
562     } finally {
563       closeConnection(response);
564     }
565     if (upperTrack.isPresent()) {
566       throw new EncoderException("Unable to composite video from track " + lowerTrack.getElement() + " and "
567               + upperTrack.get().getElement() + " using the remote composer service proxy");
568     } else {
569       throw new EncoderException("Unable to composite video from track " + lowerTrack.getElement()
570               + " using the remote composer service proxy");
571     }
572   }
573 
574   @Override
575   public Job concat(String profileId, Dimension outputDimension, boolean sameCodec, Track... tracks)
576           throws EncoderException, MediaPackageException {
577     return concat(profileId, outputDimension, -1.0f, sameCodec, tracks);
578   }
579 
580   @Override
581   public Job concat(String profileId, Dimension outputDimension, float outputFrameRate, boolean sameCodec,
582           Track... tracks)
583           throws EncoderException, MediaPackageException {
584     HttpPost post = new HttpPost("/concat");
585     try {
586       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
587       params.add(new BasicNameValuePair("profileId", profileId));
588       if (outputDimension != null) {
589         params.add(new BasicNameValuePair("outputDimension", Serializer.json(outputDimension).toJson()));
590       }
591       params.add(new BasicNameValuePair("outputFrameRate", String.format(Locale.US, "%f", outputFrameRate)));
592       params.add(new BasicNameValuePair("sourceTracks", MediaPackageElementParser.getArrayAsXml(
593           Arrays.asList(tracks))));
594       if (sameCodec) {
595         params.add(new BasicNameValuePair("sameCodec", "true"));
596       }
597       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
598     } catch (Exception e) {
599       throw new EncoderException(e);
600     }
601     HttpResponse response = null;
602     try {
603       response = getResponse(post);
604       if (response != null) {
605         Job r = JobParser.parseJob(response.getEntity().getContent());
606         logger.info("Concat video job {} started on a remote composer", r.getId());
607         return r;
608       }
609     } catch (Exception e) {
610       throw new EncoderException(e);
611     } finally {
612       closeConnection(response);
613     }
614     throw new EncoderException("Unable to concat videos from tracks " + tracks
615             + " using the remote composer service proxy");
616   }
617 
618   @Override
619   public Job imageToVideo(Attachment sourceImageAttachment, String profileId, double time) throws EncoderException,
620           MediaPackageException {
621     HttpPost post = new HttpPost("/imagetovideo");
622     try {
623       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
624       params.add(new BasicNameValuePair("sourceAttachment", MediaPackageElementParser.getAsXml(sourceImageAttachment)));
625       params.add(new BasicNameValuePair("profileId", profileId));
626       params.add(new BasicNameValuePair("time", Double.toString(time)));
627       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
628     } catch (Exception e) {
629       throw new EncoderException(e);
630     }
631     HttpResponse response = null;
632     try {
633       response = getResponse(post);
634       if (response != null) {
635         Job r = JobParser.parseJob(response.getEntity().getContent());
636         logger.info("Image to video converting job {} started on a remote composer", r.getId());
637         return r;
638       }
639     } catch (Exception e) {
640       throw new EncoderException(e);
641     } finally {
642       closeConnection(response);
643     }
644     throw new EncoderException("Unable to convert an image to a video from attachment " + sourceImageAttachment
645             + " using the remote composer service proxy");
646   }
647 
648   @Override
649   public Job demux(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException {
650     HttpPost post = new HttpPost("/demux");
651     try {
652       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
653       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
654       params.add(new BasicNameValuePair("profileId", profileId));
655       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
656     } catch (Exception e) {
657       throw new EncoderException("Unable to assemble a remote demux request for track " + sourceTrack, e);
658     }
659     HttpResponse response = null;
660     try {
661       response = getResponse(post);
662       if (response != null) {
663         String content = EntityUtils.toString(response.getEntity());
664         Job r = JobParser.parseJob(content);
665         logger.info("Demuxing job {} started on a remote service ", r.getId());
666         return r;
667       }
668     } catch (Exception e) {
669       throw new EncoderException("Unable to demux track " + sourceTrack + " using a remote composer service", e);
670     } finally {
671       closeConnection(response);
672     }
673     throw new EncoderException("Unable to demux track " + sourceTrack + " using a remote composer service");
674   }
675 
676   @Override
677   public Job processSmil(Smil smil, String trackParamGroupId, String mediaType, List<String> profileIds)
678           throws EncoderException, MediaPackageException {
679     HttpPost post = new HttpPost("/processsmil");
680     try {
681       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
682       params.add(new BasicNameValuePair("smilAsXml", smil.toXML()));
683       params.add(new BasicNameValuePair("trackId", trackParamGroupId));
684       params.add(new BasicNameValuePair("mediaType", mediaType));
685       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ","))); // comma separated profiles
686       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
687     } catch (Exception e) {
688       throw new EncoderException(e);
689     }
690     HttpResponse response = null;
691     try {
692       response = getResponse(post);
693       if (response != null) {
694         Job r = JobParser.parseJob(response.getEntity().getContent());
695         logger.info("Concat video job {} started on a remote composer", r.getId());
696         return r;
697       }
698     } catch (Exception e) {
699       throw new EncoderException(e);
700     } finally {
701       closeConnection(response);
702     }
703     throw new EncoderException("Unable to edit video group(" + trackParamGroupId + ") from smil " + smil
704             + " using the remote composer service proxy");
705   }
706 
707   @Override
708   public Job multiEncode(Track sourceTrack, List<String> profileIds) throws EncoderException, MediaPackageException {
709     HttpPost post = new HttpPost("/multiencode");
710     try {
711       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
712       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
713       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ","))); // comma separated profiles
714       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
715     } catch (Exception e) {
716       throw new EncoderException("Unable to assemble a remote demux request for track " + sourceTrack, e);
717     }
718     HttpResponse response = null;
719     try {
720       response = getResponse(post);
721       if (response != null) {
722         String content = EntityUtils.toString(response.getEntity());
723         Job job = JobParser.parseJob(content);
724         logger.info("Encoding job {} started on a remote multiencode", job.getId());
725         return job;
726       }
727     } catch (Exception e) {
728       throw new EncoderException("Unable to multiencode track " + sourceTrack + " using a remote composer service", e);
729     } finally {
730       closeConnection(response);
731     }
732     throw new EncoderException("Unable to multiencode track " + sourceTrack + " using a remote composer service");
733   }
734 
735   /**
736    * Converts a Map<String, String> to s key=value\n string, suitable for the properties form parameter expected by the
737    * workflow rest endpoint.
738    *
739    * @param props
740    *          The map of strings
741    * @return the string representation
742    */
743   private String mapToString(Map<String, String> props) {
744     StringBuilder sb = new StringBuilder();
745     for (Entry<String, String> entry : props.entrySet()) {
746       sb.append(entry.getKey());
747       sb.append("=");
748       sb.append(entry.getValue());
749       sb.append("\n");
750     }
751     return sb.toString();
752   }
753 
754 }