Basics of per-channel audio gain.
[dcpomatic.git] / src / lib / player.cc
1 /*
2     Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <stdint.h>
21 #include "player.h"
22 #include "film.h"
23 #include "ffmpeg_decoder.h"
24 #include "ffmpeg_content.h"
25 #include "image_decoder.h"
26 #include "image_content.h"
27 #include "sndfile_decoder.h"
28 #include "sndfile_content.h"
29 #include "subtitle_content.h"
30 #include "playlist.h"
31 #include "job.h"
32 #include "image.h"
33 #include "ratio.h"
34 #include "resampler.h"
35 #include "log.h"
36 #include "scaler.h"
37
38 using std::list;
39 using std::cout;
40 using std::min;
41 using std::max;
42 using std::vector;
43 using std::pair;
44 using std::map;
45 using boost::shared_ptr;
46 using boost::weak_ptr;
47 using boost::dynamic_pointer_cast;
48
49 class Piece
50 {
51 public:
52         Piece (shared_ptr<Content> c)
53                 : content (c)
54                 , video_position (c->position ())
55                 , audio_position (c->position ())
56                 , repeat_to_do (0)
57                 , repeat_done (0)
58         {}
59         
60         Piece (shared_ptr<Content> c, shared_ptr<Decoder> d)
61                 : content (c)
62                 , decoder (d)
63                 , video_position (c->position ())
64                 , audio_position (c->position ())
65         {}
66
67         /** Set this piece to repeat a video frame a given number of times */
68         void set_repeat (IncomingVideo video, int num)
69         {
70                 repeat_video = video;
71                 repeat_to_do = num;
72                 repeat_done = 0;
73         }
74
75         void reset_repeat ()
76         {
77                 repeat_video.image.reset ();
78                 repeat_to_do = 0;
79                 repeat_done = 0;
80         }
81
82         bool repeating () const
83         {
84                 return repeat_done != repeat_to_do;
85         }
86
87         void repeat (Player* player)
88         {
89                 player->process_video (
90                         repeat_video.weak_piece,
91                         repeat_video.image,
92                         repeat_video.eyes,
93                         repeat_done > 0,
94                         repeat_video.frame,
95                         (repeat_done + 1) * (TIME_HZ / player->_film->video_frame_rate ())
96                         );
97
98                 ++repeat_done;
99         }
100         
101         shared_ptr<Content> content;
102         shared_ptr<Decoder> decoder;
103         /** Time of the last video we emitted relative to the start of the DCP */
104         Time video_position;
105         /** Time of the last audio we emitted relative to the start of the DCP */
106         Time audio_position;
107
108         IncomingVideo repeat_video;
109         int repeat_to_do;
110         int repeat_done;
111 };
112
113 Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
114         : _film (f)
115         , _playlist (p)
116         , _video (true)
117         , _audio (true)
118         , _have_valid_pieces (false)
119         , _video_position (0)
120         , _audio_position (0)
121         , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
122         , _last_emit_was_black (false)
123 {
124         _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
125         _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
126         _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1));
127         set_video_container_size (fit_ratio_within (_film->container()->ratio (), _film->full_frame ()));
128 }
129
130 void
131 Player::disable_video ()
132 {
133         _video = false;
134 }
135
136 void
137 Player::disable_audio ()
138 {
139         _audio = false;
140 }
141
142 bool
143 Player::pass ()
144 {
145         if (!_have_valid_pieces) {
146                 setup_pieces ();
147         }
148
149         Time earliest_t = TIME_MAX;
150         shared_ptr<Piece> earliest;
151         enum {
152                 VIDEO,
153                 AUDIO
154         } type = VIDEO;
155
156         for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
157                 if ((*i)->decoder->done ()) {
158                         continue;
159                 }
160
161                 shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
162                 shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
163
164                 if (_video && vd) {
165                         if ((*i)->video_position < earliest_t) {
166                                 earliest_t = (*i)->video_position;
167                                 earliest = *i;
168                                 type = VIDEO;
169                         }
170                 }
171
172                 if (_audio && ad && ad->has_audio ()) {
173                         if ((*i)->audio_position < earliest_t) {
174                                 earliest_t = (*i)->audio_position;
175                                 earliest = *i;
176                                 type = AUDIO;
177                         }
178                 }
179         }
180
181         if (!earliest) {
182                 flush ();
183                 return true;
184         }
185
186         switch (type) {
187         case VIDEO:
188                 if (earliest_t > _video_position) {
189                         emit_black ();
190                 } else {
191                         if (earliest->repeating ()) {
192                                 earliest->repeat (this);
193                         } else {
194                                 earliest->decoder->pass ();
195                         }
196                 }
197                 break;
198
199         case AUDIO:
200                 if (earliest_t > _audio_position) {
201                         emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
202                 } else {
203                         earliest->decoder->pass ();
204
205                         if (earliest->decoder->done()) {
206                                 shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content);
207                                 assert (ac);
208                                 shared_ptr<Resampler> re = resampler (ac, false);
209                                 if (re) {
210                                         shared_ptr<const AudioBuffers> b = re->flush ();
211                                         if (b->frames ()) {
212                                                 process_audio (earliest, b, ac->audio_length ());
213                                         }
214                                 }
215                         }
216                 }
217                 break;
218         }
219
220         if (_audio) {
221                 boost::optional<Time> audio_done_up_to;
222                 for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
223                         if ((*i)->decoder->done ()) {
224                                 continue;
225                         }
226
227                         if (dynamic_pointer_cast<AudioDecoder> ((*i)->decoder)) {
228                                 audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position);
229                         }
230                 }
231
232                 if (audio_done_up_to) {
233                         TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ());
234                         Audio (tb.audio, tb.time);
235                         _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
236                 }
237         }
238                 
239         return false;
240 }
241
242 /** @param extra Amount of extra time to add to the content frame's time (for repeat) */
243 void
244 Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const Image> image, Eyes eyes, bool same, VideoContent::Frame frame, Time extra)
245 {
246         /* Keep a note of what came in so that we can repeat it if required */
247         _last_incoming_video.weak_piece = weak_piece;
248         _last_incoming_video.image = image;
249         _last_incoming_video.eyes = eyes;
250         _last_incoming_video.same = same;
251         _last_incoming_video.frame = frame;
252         _last_incoming_video.extra = extra;
253         
254         shared_ptr<Piece> piece = weak_piece.lock ();
255         if (!piece) {
256                 return;
257         }
258
259         shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
260         assert (content);
261
262         FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
263         if (frc.skip && (frame % 2) == 1) {
264                 return;
265         }
266
267         Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
268         if (content->trimmed (relative_time)) {
269                 return;
270         }
271
272         Time const time = content->position() + relative_time + extra - content->trim_start ();
273         float const ratio = content->ratio() ? content->ratio()->ratio() : content->video_size_after_crop().ratio();
274         libdcp::Size const image_size = fit_ratio_within (ratio, _video_container_size);
275
276         shared_ptr<PlayerImage> pi (
277                 new PlayerImage (
278                         image,
279                         content->crop(),
280                         image_size,
281                         _video_container_size,
282                         _film->scaler()
283                         )
284                 );
285         
286         if (_film->with_subtitles () && _out_subtitle.image && time >= _out_subtitle.from && time <= _out_subtitle.to) {
287
288                 Position<int> const container_offset (
289                         (_video_container_size.width - image_size.width) / 2,
290                         (_video_container_size.height - image_size.width) / 2
291                         );
292
293                 pi->set_subtitle (_out_subtitle.image, _out_subtitle.position + container_offset);
294         }
295                                             
296 #ifdef DCPOMATIC_DEBUG
297         _last_video = piece->content;
298 #endif
299
300         Video (pi, eyes, content->colour_conversion(), same, time);
301
302         _last_emit_was_black = false;
303         _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
304
305         if (frc.repeat > 1 && !piece->repeating ()) {
306                 piece->set_repeat (_last_incoming_video, frc.repeat - 1);
307         }
308 }
309
310 void
311 Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame)
312 {
313         shared_ptr<Piece> piece = weak_piece.lock ();
314         if (!piece) {
315                 return;
316         }
317
318         shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content);
319         assert (content);
320
321         /* Gain */
322         if (content->audio_gain() != 0) {
323                 shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
324                 gain->apply_gain (content->audio_gain ());
325                 audio = gain;
326         }
327
328         /* Resample */
329         if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) {
330                 shared_ptr<Resampler> r = resampler (content, true);
331                 pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame);
332                 audio = ro.first;
333                 frame = ro.second;
334         }
335         
336         Time const relative_time = _film->audio_frames_to_time (frame);
337
338         if (content->trimmed (relative_time)) {
339                 return;
340         }
341
342         Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
343         
344         /* Remap channels */
345         shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames()));
346         dcp_mapped->make_silent ();
347
348         AudioMapping map = content->audio_mapping ();
349         for (int i = 0; i < map.content_channels(); ++i) {
350                 for (int j = 0; j < _film->audio_channels(); ++j) {
351                         if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) {
352                                 dcp_mapped->accumulate_channel (
353                                         audio.get(),
354                                         i,
355                                         static_cast<libdcp::Channel> (j),
356                                         map.get (i, static_cast<libdcp::Channel> (j))
357                                         );
358                         }
359                 }
360         }
361
362         audio = dcp_mapped;
363
364         /* We must cut off anything that comes before the start of all time */
365         if (time < 0) {
366                 int const frames = - time * _film->audio_frame_rate() / TIME_HZ;
367                 if (frames >= audio->frames ()) {
368                         return;
369                 }
370
371                 shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
372                 trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
373
374                 audio = trimmed;
375                 time = 0;
376         }
377
378         _audio_merger.push (audio, time);
379         piece->audio_position += _film->audio_frames_to_time (audio->frames ());
380 }
381
382 void
383 Player::flush ()
384 {
385         TimedAudioBuffers<Time> tb = _audio_merger.flush ();
386         if (_audio && tb.audio) {
387                 Audio (tb.audio, tb.time);
388                 _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
389         }
390
391         while (_video && _video_position < _audio_position) {
392                 emit_black ();
393         }
394
395         while (_audio && _audio_position < _video_position) {
396                 emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
397         }
398         
399 }
400
401 /** Seek so that the next pass() will yield (approximately) the requested frame.
402  *  Pass accurate = true to try harder to get close to the request.
403  *  @return true on error
404  */
405 void
406 Player::seek (Time t, bool accurate)
407 {
408         if (!_have_valid_pieces) {
409                 setup_pieces ();
410         }
411
412         if (_pieces.empty ()) {
413                 return;
414         }
415
416         for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
417                 shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
418                 if (!vc) {
419                         continue;
420                 }
421
422                 /* s is the offset of t from the start position of this content */
423                 Time s = t - vc->position ();
424                 s = max (static_cast<Time> (0), s);
425                 s = min (vc->length_after_trim(), s);
426
427                 /* Hence set the piece positions to the `global' time */
428                 (*i)->video_position = (*i)->audio_position = vc->position() + s;
429
430                 /* And seek the decoder */
431                 dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
432                         vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
433                         );
434
435                 (*i)->reset_repeat ();
436         }
437
438         _video_position = _audio_position = t;
439
440         /* XXX: don't seek audio because we don't need to... */
441 }
442
443 void
444 Player::setup_pieces ()
445 {
446         list<shared_ptr<Piece> > old_pieces = _pieces;
447
448         _pieces.clear ();
449
450         ContentList content = _playlist->content ();
451         sort (content.begin(), content.end(), ContentSorter ());
452
453         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
454
455                 shared_ptr<Piece> piece (new Piece (*i));
456
457                 /* XXX: into content? */
458
459                 shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
460                 if (fc) {
461                         shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio));
462                         
463                         fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
464                         fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
465                         fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
466
467                         fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
468                         piece->decoder = fd;
469                 }
470                 
471                 shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
472                 if (ic) {
473                         bool reusing = false;
474                         
475                         /* See if we can re-use an old ImageDecoder */
476                         for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
477                                 shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
478                                 if (imd && imd->content() == ic) {
479                                         piece = *j;
480                                         reusing = true;
481                                 }
482                         }
483
484                         if (!reusing) {
485                                 shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic));
486                                 id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
487                                 piece->decoder = id;
488                         }
489                 }
490
491                 shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
492                 if (sc) {
493                         shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc));
494                         sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
495
496                         piece->decoder = sd;
497                 }
498
499                 _pieces.push_back (piece);
500         }
501
502         _have_valid_pieces = true;
503 }
504
505 void
506 Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
507 {
508         shared_ptr<Content> c = w.lock ();
509         if (!c) {
510                 return;
511         }
512
513         if (
514                 property == ContentProperty::POSITION || property == ContentProperty::LENGTH ||
515                 property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END ||
516                 property == VideoContentProperty::VIDEO_FRAME_TYPE 
517                 ) {
518                 
519                 _have_valid_pieces = false;
520                 Changed (frequent);
521
522         } else if (property == SubtitleContentProperty::SUBTITLE_OFFSET || property == SubtitleContentProperty::SUBTITLE_SCALE) {
523
524                 update_subtitle ();
525                 Changed (frequent);
526
527         } else if (property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_RATIO) {
528                 
529                 Changed (frequent);
530
531         } else if (property == ContentProperty::PATH) {
532
533                 Changed (frequent);
534         }
535 }
536
537 void
538 Player::playlist_changed ()
539 {
540         _have_valid_pieces = false;
541         Changed (false);
542 }
543
544 void
545 Player::set_video_container_size (libdcp::Size s)
546 {
547         _video_container_size = s;
548
549         shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true));
550         im->make_black ();
551         
552         _black_frame.reset (
553                 new PlayerImage (
554                         im,
555                         Crop(),
556                         _video_container_size,
557                         _video_container_size,
558                         Scaler::from_id ("bicubic")
559                         )
560                 );
561 }
562
563 shared_ptr<Resampler>
564 Player::resampler (shared_ptr<AudioContent> c, bool create)
565 {
566         map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
567         if (i != _resamplers.end ()) {
568                 return i->second;
569         }
570
571         if (!create) {
572                 return shared_ptr<Resampler> ();
573         }
574
575         _film->log()->log (
576                 String::compose (
577                         "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
578                         )
579                 );
580         
581         shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
582         _resamplers[c] = r;
583         return r;
584 }
585
586 void
587 Player::emit_black ()
588 {
589 #ifdef DCPOMATIC_DEBUG
590         _last_video.reset ();
591 #endif
592
593         Video (_black_frame, EYES_BOTH, ColourConversion(), _last_emit_was_black, _video_position);
594         _video_position += _film->video_frames_to_time (1);
595         _last_emit_was_black = true;
596 }
597
598 void
599 Player::emit_silence (OutputAudioFrame most)
600 {
601         if (most == 0) {
602                 return;
603         }
604         
605         OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
606         shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
607         silence->make_silent ();
608         Audio (silence, _audio_position);
609         _audio_position += _film->audio_frames_to_time (N);
610 }
611
612 void
613 Player::film_changed (Film::Property p)
614 {
615         /* Here we should notice Film properties that affect our output, and
616            alert listeners that our output now would be different to how it was
617            last time we were run.
618         */
619
620         if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER) {
621                 Changed (false);
622         }
623 }
624
625 void
626 Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
627 {
628         _in_subtitle.piece = weak_piece;
629         _in_subtitle.image = image;
630         _in_subtitle.rect = rect;
631         _in_subtitle.from = from;
632         _in_subtitle.to = to;
633
634         update_subtitle ();
635 }
636
637 void
638 Player::update_subtitle ()
639 {
640         shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
641         if (!piece) {
642                 return;
643         }
644
645         if (!_in_subtitle.image) {
646                 _out_subtitle.image.reset ();
647                 return;
648         }
649
650         shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
651         assert (sc);
652
653         dcpomatic::Rect<double> in_rect = _in_subtitle.rect;
654         libdcp::Size scaled_size;
655
656         in_rect.y += sc->subtitle_offset ();
657
658         /* We will scale the subtitle up to fit _video_container_size, and also by the additional subtitle_scale */
659         scaled_size.width = in_rect.width * _video_container_size.width * sc->subtitle_scale ();
660         scaled_size.height = in_rect.height * _video_container_size.height * sc->subtitle_scale ();
661
662         /* Then we need a corrective translation, consisting of two parts:
663          *
664          * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
665          *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
666          *
667          * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
668          *     (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and
669          *     (height_before_subtitle_scale * (1 - subtitle_scale) / 2).
670          *
671          * Combining these two translations gives these expressions.
672          */
673         
674         _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
675         _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
676         
677         _out_subtitle.image = _in_subtitle.image->scale (
678                 scaled_size,
679                 Scaler::from_id ("bicubic"),
680                 _in_subtitle.image->pixel_format (),
681                 true
682                 );
683         _out_subtitle.from = _in_subtitle.from + piece->content->position ();
684         _out_subtitle.to = _in_subtitle.to + piece->content->position ();
685 }
686
687 /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
688  *  @return false if this could not be done.
689  */
690 bool
691 Player::repeat_last_video ()
692 {
693         if (!_last_incoming_video.image || !_have_valid_pieces) {
694                 return false;
695         }
696
697         process_video (
698                 _last_incoming_video.weak_piece,
699                 _last_incoming_video.image,
700                 _last_incoming_video.eyes,
701                 _last_incoming_video.same,
702                 _last_incoming_video.frame,
703                 _last_incoming_video.extra
704                 );
705
706         return true;
707 }
708
709 PlayerImage::PlayerImage (
710         shared_ptr<const Image> in,
711         Crop crop,
712         libdcp::Size inter_size,
713         libdcp::Size out_size,
714         Scaler const * scaler
715         )
716         : _in (in)
717         , _crop (crop)
718         , _inter_size (inter_size)
719         , _out_size (out_size)
720         , _scaler (scaler)
721 {
722
723 }
724
725 void
726 PlayerImage::set_subtitle (shared_ptr<const Image> image, Position<int> pos)
727 {
728         _subtitle_image = image;
729         _subtitle_position = pos;
730 }
731
732 shared_ptr<Image>
733 PlayerImage::image ()
734 {
735         shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
736
737         Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
738
739         if (_subtitle_image) {
740                 out->alpha_blend (_subtitle_image, _subtitle_position);
741         }
742
743         return out;
744 }