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  import org.opencastproject.util.data.Option;
44  
45  import org.apache.commons.io.IOUtils;
46  import org.apache.commons.lang3.StringUtils;
47  import org.apache.http.HttpResponse;
48  import org.apache.http.HttpStatus;
49  import org.apache.http.client.entity.UrlEncodedFormEntity;
50  import org.apache.http.client.methods.HttpGet;
51  import org.apache.http.client.methods.HttpPost;
52  import org.apache.http.message.BasicNameValuePair;
53  import org.apache.http.util.EntityUtils;
54  import org.osgi.service.component.annotations.Component;
55  import org.osgi.service.component.annotations.Reference;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  import java.io.IOException;
60  import java.nio.charset.Charset;
61  import java.util.ArrayList;
62  import java.util.Arrays;
63  import java.util.List;
64  import java.util.Locale;
65  import java.util.Map;
66  import java.util.Map.Entry;
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#getProfile(java.lang.String)
250    */
251   @Override
252   public EncodingProfile getProfile(String profileId) {
253     HttpGet get = new HttpGet("/profile/" + profileId + ".xml");
254     HttpResponse response = null;
255     try {
256       response = getResponse(get, HttpStatus.SC_OK, HttpStatus.SC_NOT_FOUND);
257       if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
258         return EncodingProfileBuilder.getInstance().parseProfile(response.getEntity().getContent());
259       } else {
260         return null;
261       }
262     } catch (Exception e) {
263       throw new RuntimeException(e);
264     } finally {
265       closeConnection(response);
266     }
267   }
268 
269   /**
270    * {@inheritDoc}
271    */
272   @Override
273   public Job image(Track sourceTrack, String profileId, double... times) throws EncoderException {
274     HttpPost post = new HttpPost("/image");
275     try {
276       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
277       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
278       params.add(new BasicNameValuePair("profileId", profileId));
279       params.add(new BasicNameValuePair("time", buildTimeArray(times)));
280       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
281     } catch (Exception e) {
282       throw new EncoderException(e);
283     }
284     HttpResponse response = null;
285     try {
286       response = getResponse(post);
287       if (response != null) {
288         Job r = JobParser.parseJob(response.getEntity().getContent());
289         logger.info("Image extraction job {} started on a remote composer", r.getId());
290         return r;
291       }
292     } catch (Exception e) {
293       throw new EncoderException(e);
294     } finally {
295       closeConnection(response);
296     }
297     throw new EncoderException("Unable to compose an image from track " + sourceTrack
298             + " using the remote composer service proxy");
299   }
300 
301   @Override
302   public List<Attachment> imageSync(Track sourceTrack, String profileId, double... times) throws EncoderException, MediaPackageException {
303     HttpPost post = new HttpPost("/imagesync");
304     try {
305       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
306       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
307       params.add(new BasicNameValuePair("profileId", profileId));
308       params.add(new BasicNameValuePair("time", buildTimeArray(times)));
309       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
310     } catch (Exception e) {
311       throw new EncoderException(e);
312     }
313     HttpResponse response = null;
314     try {
315       response = getResponse(post);
316       if (response != null) {
317         final String xml = IOUtils.toString(response.getEntity().getContent(), Charset.forName("utf-8"));
318         return MediaPackageElementParser.getArrayFromXml(xml)
319             .stream().map(e -> (Attachment)e)
320             .collect(Collectors.toList());
321       }
322     } catch (Exception e) {
323       throw new EncoderException(e);
324     } finally {
325       closeConnection(response);
326     }
327     throw new EncoderException("Unable to compose an image from track " + sourceTrack
328         + " using the remote composer service proxy");
329   }
330 
331   /**
332    * {@inheritDoc}
333    *
334    * @see org.opencastproject.composer.api.ComposerService#image(Track, String, Map)
335    */
336   @Override
337   public Job image(Track sourceTrack, String profileId, Map<String, String> properties) throws EncoderException,
338           MediaPackageException {
339     HttpPost post = new HttpPost("/image");
340     try {
341       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
342       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
343       params.add(new BasicNameValuePair("profileId", profileId));
344       if (properties != null)
345         params.add(new BasicNameValuePair("properties", mapToString(properties)));
346       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
347     } catch (Exception e) {
348       throw new EncoderException(e);
349     }
350     HttpResponse response = null;
351     try {
352       response = getResponse(post);
353       if (response != null) {
354         Job r = JobParser.parseJob(response.getEntity().getContent());
355         logger.info("Image extraction job {} started on a remote composer", r.getId());
356         return r;
357       }
358     } catch (Exception e) {
359       throw new EncoderException(e);
360     } finally {
361       closeConnection(response);
362     }
363     throw new EncoderException("Unable to compose an image from track " + sourceTrack
364             + " using the remote composer service proxy");
365   }
366 
367   /**
368    * {@inheritDoc}
369    *
370    * @see org.opencastproject.composer.api.ComposerService#convertImage(org.opencastproject.mediapackage.Attachment,
371    *      java.lang.String...)
372    */
373   @Override
374   public Job convertImage(Attachment image, String... profileIds) throws EncoderException, MediaPackageException {
375     HttpPost post = new HttpPost("/convertimage");
376     try {
377       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
378       params.add(new BasicNameValuePair("sourceImage", MediaPackageElementParser.getAsXml(image)));
379       params.add(new BasicNameValuePair("profileId", StringUtils.join(profileIds, ',')));
380       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
381     } catch (Exception e) {
382       throw new EncoderException(e);
383     }
384     HttpResponse response = null;
385     try {
386       response = getResponse(post);
387       if (response != null) {
388         Job r = JobParser.parseJob(response.getEntity().getContent());
389         logger.info("Image conversion job {} started on a remote composer", r.getId());
390         return r;
391       }
392     } catch (Exception e) {
393       throw new EncoderException(e);
394     } finally {
395       closeConnection(response);
396     }
397     throw new EncoderException("Unable to convert image at " + image + " using the remote composer service proxy");
398   }
399 
400   /**
401    * {@inheritDoc}
402    *
403    * @see org.opencastproject.composer.api.ComposerService#convertImageSync(
404    *      org.opencastproject.mediapackage.Attachment, java.lang.String...)
405    */
406   @Override
407   public List<Attachment> convertImageSync(Attachment image, String... profileIds) throws EncoderException, MediaPackageException {
408     HttpPost post = new HttpPost("/convertimagesync");
409     try {
410       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
411       params.add(new BasicNameValuePair("sourceImage", MediaPackageElementParser.getAsXml(image)));
412       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ',')));
413       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
414     } catch (Exception e) {
415       throw new EncoderException(e);
416     }
417     HttpResponse response = null;
418     try {
419       response = getResponse(post);
420       if (response != null) {
421         final String xml = IOUtils.toString(response.getEntity().getContent(), Charset.forName("utf-8"));
422         return MediaPackageElementParser.getArrayFromXml(xml).stream().map(a -> (Attachment) a).collect(Collectors.toList());
423       }
424     } catch (Exception e) {
425       throw new EncoderException(e);
426     } finally {
427       closeConnection(response);
428     }
429     throw new EncoderException("Unable to convert image at " + image + " using the remote composer service proxy");
430   }
431 
432   /**
433    * {@inheritDoc}
434    *
435    * @see org.opencastproject.composer.api.ComposerService#listProfiles()
436    */
437   @Override
438   public EncodingProfile[] listProfiles() {
439     HttpGet get = new HttpGet("/profiles.xml");
440     HttpResponse response = null;
441     try {
442       response = getResponse(get);
443       if (response != null) {
444         EncodingProfileList profileList = EncodingProfileBuilder.getInstance().parseProfileList(
445                 response.getEntity().getContent());
446         List<EncodingProfileImpl> list = profileList.getProfiles();
447         return list.toArray(new EncodingProfile[list.size()]);
448       }
449     } catch (Exception e) {
450       throw new RuntimeException(
451               "Unable to list the encoding profiles registered with the remote composer service proxy", e);
452     } finally {
453       closeConnection(response);
454     }
455     throw new RuntimeException("Unable to list the encoding profiles registered with the remote composer service proxy");
456   }
457 
458   /**
459    * Builds string containing times in seconds separated by comma.
460    *
461    * @param times
462    *          time array to be converted to string
463    * @return string represented specified time array
464    */
465   protected String buildTimeArray(double[] times) {
466     if (times.length == 0)
467       return "";
468 
469     StringBuilder builder = new StringBuilder();
470     builder.append(Double.toString(times[0]));
471     for (int i = 1; i < times.length; i++) {
472       builder.append(";" + Double.toString(times[i]));
473     }
474     return builder.toString();
475   }
476 
477   @Override
478   public Job composite(Dimension compositeTrackSize, Option<LaidOutElement<Track>> upperTrack,
479           LaidOutElement<Track> lowerTrack, Option<LaidOutElement<Attachment>> watermark, String profileId,
480           String background, String sourceAudioName) throws EncoderException, MediaPackageException {
481     HttpPost post = new HttpPost("/composite");
482     try {
483       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
484       params.add(new BasicNameValuePair("compositeSize", Serializer.json(compositeTrackSize).toJson()));
485       params.add(new BasicNameValuePair("lowerTrack", MediaPackageElementParser.getAsXml(lowerTrack.getElement())));
486       params.add(new BasicNameValuePair("lowerLayout", Serializer.json(lowerTrack.getLayout()).toJson()));
487       if (upperTrack.isSome()) {
488         params.add(new BasicNameValuePair("upperTrack", MediaPackageElementParser.getAsXml(upperTrack.get()
489                 .getElement())));
490         params.add(new BasicNameValuePair("upperLayout", Serializer.json(upperTrack.get().getLayout()).toJson()));
491       }
492 
493       if (watermark.isSome()) {
494         params.add(new BasicNameValuePair("watermarkAttachment", MediaPackageElementParser.getAsXml(watermark.get()
495                 .getElement())));
496         params.add(new BasicNameValuePair("watermarkLayout", Serializer.json(watermark.get().getLayout()).toJson()));
497       }
498       params.add(new BasicNameValuePair("profileId", profileId));
499       params.add(new BasicNameValuePair("background", background));
500       params.add(new BasicNameValuePair("sourceAudioName", sourceAudioName));
501       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
502     } catch (Exception e) {
503       throw new EncoderException(e);
504     }
505     HttpResponse response = null;
506     try {
507       response = getResponse(post);
508       if (response != null) {
509         Job r = JobParser.parseJob(response.getEntity().getContent());
510         logger.info("Composite video job {} started on a remote composer", r.getId());
511         return r;
512       }
513     } catch (Exception e) {
514       throw new EncoderException(e);
515     } finally {
516       closeConnection(response);
517     }
518     if (upperTrack.isSome()) {
519       throw new EncoderException("Unable to composite video from track " + lowerTrack.getElement() + " and "
520               + upperTrack.get().getElement() + " using the remote composer service proxy");
521     } else {
522       throw new EncoderException("Unable to composite video from track " + lowerTrack.getElement()
523               + " using the remote composer service proxy");
524     }
525   }
526 
527   @Override
528   public Job concat(String profileId, Dimension outputDimension, boolean sameCodec, Track... tracks)
529           throws EncoderException, MediaPackageException {
530     return concat(profileId, outputDimension, -1.0f, sameCodec, tracks);
531   }
532 
533   @Override
534   public Job concat(String profileId, Dimension outputDimension, float outputFrameRate, boolean sameCodec,
535           Track... tracks)
536           throws EncoderException, MediaPackageException {
537     HttpPost post = new HttpPost("/concat");
538     try {
539       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
540       params.add(new BasicNameValuePair("profileId", profileId));
541       if (outputDimension != null)
542         params.add(new BasicNameValuePair("outputDimension", Serializer.json(outputDimension).toJson()));
543       params.add(new BasicNameValuePair("outputFrameRate", String.format(Locale.US, "%f", outputFrameRate)));
544       params.add(new BasicNameValuePair("sourceTracks", MediaPackageElementParser.getArrayAsXml(Arrays.asList(tracks))));
545       if (sameCodec)
546         params.add(new BasicNameValuePair("sameCodec", "true"));
547       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
548     } catch (Exception e) {
549       throw new EncoderException(e);
550     }
551     HttpResponse response = null;
552     try {
553       response = getResponse(post);
554       if (response != null) {
555         Job r = JobParser.parseJob(response.getEntity().getContent());
556         logger.info("Concat video job {} started on a remote composer", r.getId());
557         return r;
558       }
559     } catch (Exception e) {
560       throw new EncoderException(e);
561     } finally {
562       closeConnection(response);
563     }
564     throw new EncoderException("Unable to concat videos from tracks " + tracks
565             + " using the remote composer service proxy");
566   }
567 
568   @Override
569   public Job imageToVideo(Attachment sourceImageAttachment, String profileId, double time) throws EncoderException,
570           MediaPackageException {
571     HttpPost post = new HttpPost("/imagetovideo");
572     try {
573       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
574       params.add(new BasicNameValuePair("sourceAttachment", MediaPackageElementParser.getAsXml(sourceImageAttachment)));
575       params.add(new BasicNameValuePair("profileId", profileId));
576       params.add(new BasicNameValuePair("time", Double.toString(time)));
577       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
578     } catch (Exception e) {
579       throw new EncoderException(e);
580     }
581     HttpResponse response = null;
582     try {
583       response = getResponse(post);
584       if (response != null) {
585         Job r = JobParser.parseJob(response.getEntity().getContent());
586         logger.info("Image to video converting job {} started on a remote composer", r.getId());
587         return r;
588       }
589     } catch (Exception e) {
590       throw new EncoderException(e);
591     } finally {
592       closeConnection(response);
593     }
594     throw new EncoderException("Unable to convert an image to a video from attachment " + sourceImageAttachment
595             + " using the remote composer service proxy");
596   }
597 
598   @Override
599   public Job demux(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException {
600     HttpPost post = new HttpPost("/demux");
601     try {
602       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
603       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
604       params.add(new BasicNameValuePair("profileId", profileId));
605       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
606     } catch (Exception e) {
607       throw new EncoderException("Unable to assemble a remote demux request for track " + sourceTrack, e);
608     }
609     HttpResponse response = null;
610     try {
611       response = getResponse(post);
612       if (response != null) {
613         String content = EntityUtils.toString(response.getEntity());
614         Job r = JobParser.parseJob(content);
615         logger.info("Demuxing job {} started on a remote service ", r.getId());
616         return r;
617       }
618     } catch (Exception e) {
619       throw new EncoderException("Unable to demux track " + sourceTrack + " using a remote composer service", e);
620     } finally {
621       closeConnection(response);
622     }
623     throw new EncoderException("Unable to demux track " + sourceTrack + " using a remote composer service");
624   }
625 
626   @Override
627   public Job processSmil(Smil smil, String trackParamGroupId, String mediaType, List<String> profileIds)
628           throws EncoderException, MediaPackageException {
629     HttpPost post = new HttpPost("/processsmil");
630     try {
631       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
632       params.add(new BasicNameValuePair("smilAsXml", smil.toXML()));
633       params.add(new BasicNameValuePair("trackId", trackParamGroupId));
634       params.add(new BasicNameValuePair("mediaType", mediaType));
635       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ","))); // comma separated profiles
636       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
637     } catch (Exception e) {
638       throw new EncoderException(e);
639     }
640     HttpResponse response = null;
641     try {
642       response = getResponse(post);
643       if (response != null) {
644         Job r = JobParser.parseJob(response.getEntity().getContent());
645         logger.info("Concat video job {} started on a remote composer", r.getId());
646         return r;
647       }
648     } catch (Exception e) {
649       throw new EncoderException(e);
650     } finally {
651       closeConnection(response);
652     }
653     throw new EncoderException("Unable to edit video group(" + trackParamGroupId + ") from smil " + smil
654             + " using the remote composer service proxy");
655   }
656 
657   @Override
658   public Job multiEncode(Track sourceTrack, List<String> profileIds) throws EncoderException, MediaPackageException {
659     HttpPost post = new HttpPost("/multiencode");
660     try {
661       List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
662       params.add(new BasicNameValuePair("sourceTrack", MediaPackageElementParser.getAsXml(sourceTrack)));
663       params.add(new BasicNameValuePair("profileIds", StringUtils.join(profileIds, ","))); // comma separated profiles
664       post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
665     } catch (Exception e) {
666       throw new EncoderException("Unable to assemble a remote demux request for track " + sourceTrack, e);
667     }
668     HttpResponse response = null;
669     try {
670       response = getResponse(post);
671       if (response != null) {
672         String content = EntityUtils.toString(response.getEntity());
673         Job job = JobParser.parseJob(content);
674         logger.info("Encoding job {} started on a remote multiencode", job.getId());
675         return job;
676       }
677     } catch (Exception e) {
678       throw new EncoderException("Unable to multiencode track " + sourceTrack + " using a remote composer service", e);
679     } finally {
680       closeConnection(response);
681     }
682     throw new EncoderException("Unable to multiencode track " + sourceTrack + " using a remote composer service");
683   }
684 
685   /**
686    * Converts a Map<String, String> to s key=value\n string, suitable for the properties form parameter expected by the
687    * workflow rest endpoint.
688    *
689    * @param props
690    *          The map of strings
691    * @return the string representation
692    */
693   private String mapToString(Map<String, String> props) {
694     StringBuilder sb = new StringBuilder();
695     for (Entry<String, String> entry : props.entrySet()) {
696       sb.append(entry.getKey());
697       sb.append("=");
698       sb.append(entry.getValue());
699       sb.append("\n");
700     }
701     return sb.toString();
702   }
703 
704 }