Various attempted fixes to audio sync.
[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 <algorithm>
22 #include "player.h"
23 #include "film.h"
24 #include "ffmpeg_decoder.h"
25 #include "ffmpeg_content.h"
26 #include "image_decoder.h"
27 #include "image_content.h"
28 #include "sndfile_decoder.h"
29 #include "sndfile_content.h"
30 #include "subtitle_content.h"
31 #include "playlist.h"
32 #include "job.h"
33 #include "image.h"
34 #include "ratio.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 using boost::optional;
49
50 class Piece
51 {
52 public:
53         Piece (shared_ptr<Content> c, shared_ptr<Decoder> d, FrameRateChange f)
54                 : content (c)
55                 , decoder (d)
56                 , frc (f)
57         {}
58
59         shared_ptr<Content> content;
60         shared_ptr<Decoder> decoder;
61         FrameRateChange frc;
62 };
63
64 Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
65         : _film (f)
66         , _playlist (p)
67         , _video (true)
68         , _audio (true)
69         , _have_valid_pieces (false)
70         , _video_position (0)
71         , _audio_position (0)
72         , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
73         , _last_emit_was_black (false)
74         , _just_did_inaccurate_seek (false)
75         , _approximate_size (false)
76 {
77         _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
78         _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
79         _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1));
80         set_video_container_size (fit_ratio_within (_film->container()->ratio (), _film->full_frame ()));
81 }
82
83 void
84 Player::disable_video ()
85 {
86         _video = false;
87 }
88
89 void
90 Player::disable_audio ()
91 {
92         _audio = false;
93 }
94
95 bool
96 Player::pass ()
97 {
98         if (!_have_valid_pieces) {
99                 setup_pieces ();
100         }
101
102         /* Interrogate all our pieces to find the one with the earliest decoded data */
103
104         shared_ptr<Piece> earliest_piece;
105         shared_ptr<Decoded> earliest_decoded;
106         DCPTime earliest_time = TIME_MAX;
107         DCPTime earliest_audio = TIME_MAX;
108
109         for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
110
111                 DCPTime const offset = (*i)->content->position() - (*i)->content->trim_start();
112                 
113                 bool done = false;
114                 shared_ptr<Decoded> dec;
115                 while (!done) {
116                         dec = (*i)->decoder->peek ();
117                         if (!dec) {
118                                 /* Decoder has nothing else to give us */
119                                 break;
120                         }
121
122                         dec->set_dcp_times (_film->video_frame_rate(), _film->audio_frame_rate(), (*i)->frc, offset);
123                         DCPTime const t = dec->dcp_time - offset;
124                         if (t >= ((*i)->content->full_length() - (*i)->content->trim_end ())) {
125                                 /* In the end-trimmed part; decoder has nothing else to give us */
126                                 dec.reset ();
127                                 done = true;
128                         } else if (t >= (*i)->content->trim_start ()) {
129                                 /* Within the un-trimmed part; everything's ok */
130                                 done = true;
131                         } else {
132                                 /* Within the start-trimmed part; get something else */
133                                 (*i)->decoder->consume ();
134                         }
135                 }
136
137                 if (!dec) {
138                         continue;
139                 }
140
141                 if (dec->dcp_time < earliest_time) {
142                         earliest_piece = *i;
143                         earliest_decoded = dec;
144                         earliest_time = dec->dcp_time;
145                 }
146
147                 if (dynamic_pointer_cast<DecodedAudio> (dec) && dec->dcp_time < earliest_audio) {
148                         earliest_audio = dec->dcp_time;
149                 }
150         }
151                 
152         if (!earliest_piece) {
153                 flush ();
154                 return true;
155         }
156
157         if (earliest_audio != TIME_MAX) {
158                 TimedAudioBuffers<DCPTime> tb = _audio_merger.pull (max (int64_t (0), earliest_audio));
159                 Audio (tb.audio, tb.time);
160                 /* This assumes that the audio_frames_to_time conversion is exact
161                    so that there are no accumulated errors caused by rounding.
162                 */
163                 _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
164         }
165
166         /* Emit the earliest thing */
167
168         shared_ptr<DecodedVideo> dv = dynamic_pointer_cast<DecodedVideo> (earliest_decoded);
169         shared_ptr<DecodedAudio> da = dynamic_pointer_cast<DecodedAudio> (earliest_decoded);
170         shared_ptr<DecodedSubtitle> ds = dynamic_pointer_cast<DecodedSubtitle> (earliest_decoded);
171
172         /* Will be set to false if we shouldn't consume the peeked DecodedThing */
173         bool consume = true;
174
175         if (dv && _video) {
176
177                 if (_just_did_inaccurate_seek) {
178
179                         /* Just emit; no subtlety */
180                         emit_video (earliest_piece, dv);
181                         step_video_position (dv);
182                         
183                 } else if (dv->dcp_time > _video_position) {
184
185                         /* Too far ahead */
186
187                         list<shared_ptr<Piece> >::iterator i = _pieces.begin();
188                         while (i != _pieces.end() && ((*i)->content->position() >= _video_position || _video_position >= (*i)->content->end())) {
189                                 ++i;
190                         }
191
192                         if (i == _pieces.end() || !_last_incoming_video.video || !_have_valid_pieces) {
193                                 /* We're outside all video content */
194                                 emit_black ();
195                                 _statistics.video.black++;
196                         } else {
197                                 /* We're inside some video; repeat the frame */
198                                 _last_incoming_video.video->dcp_time = _video_position;
199                                 emit_video (_last_incoming_video.weak_piece, _last_incoming_video.video);
200                                 step_video_position (_last_incoming_video.video);
201                                 _statistics.video.repeat++;
202                         }
203
204                         consume = false;
205
206                 } else if (dv->dcp_time == _video_position) {
207                         /* We're ok */
208                         emit_video (earliest_piece, dv);
209                         step_video_position (dv);
210                         _statistics.video.good++;
211                 } else {
212                         /* Too far behind: skip */
213                         _statistics.video.skip++;
214                 }
215
216                 _just_did_inaccurate_seek = false;
217
218         } else if (da && _audio) {
219
220                 if (da->dcp_time > _audio_position) {
221                         /* Too far ahead */
222                         emit_silence (da->dcp_time - _audio_position);
223                         consume = false;
224                         _statistics.audio.silence += (da->dcp_time - _audio_position);
225                 } else if (da->dcp_time == _audio_position) {
226                         /* We're ok */
227                         emit_audio (earliest_piece, da);
228                         _statistics.audio.good += da->data->frames();
229                 } else {
230                         /* Too far behind: skip */
231                         _statistics.audio.skip += da->data->frames();
232                 }
233                 
234         } else if (ds && _video) {
235                 _in_subtitle.piece = earliest_piece;
236                 _in_subtitle.subtitle = ds;
237                 update_subtitle ();
238         }
239
240         if (consume) {
241                 earliest_piece->decoder->consume ();
242         }                       
243         
244         return false;
245 }
246
247 void
248 Player::emit_video (weak_ptr<Piece> weak_piece, shared_ptr<DecodedVideo> video)
249 {
250         /* Keep a note of what came in so that we can repeat it if required */
251         _last_incoming_video.weak_piece = weak_piece;
252         _last_incoming_video.video = video;
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         FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
263
264         float const ratio = content->ratio() ? content->ratio()->ratio() : content->video_size_after_crop().ratio();
265         libdcp::Size image_size = fit_ratio_within (ratio, _video_container_size);
266         if (_approximate_size) {
267                 image_size.width &= ~3;
268                 image_size.height &= ~3;
269         }
270
271         shared_ptr<PlayerImage> pi (
272                 new PlayerImage (
273                         video->image,
274                         content->crop(),
275                         image_size,
276                         _video_container_size,
277                         _film->scaler()
278                         )
279                 );
280         
281         if (
282                 _film->with_subtitles () &&
283                 _out_subtitle.image &&
284                 video->dcp_time >= _out_subtitle.from && video->dcp_time <= _out_subtitle.to
285                 ) {
286
287                 Position<int> const container_offset (
288                         (_video_container_size.width - image_size.width) / 2,
289                         (_video_container_size.height - image_size.height) / 2
290                         );
291
292                 pi->set_subtitle (_out_subtitle.image, _out_subtitle.position + container_offset);
293         }
294                                             
295 #ifdef DCPOMATIC_DEBUG
296         _last_video = piece->content;
297 #endif
298
299         Video (pi, video->eyes, content->colour_conversion(), video->same, video->dcp_time);
300         
301         _last_emit_was_black = false;
302 }
303
304 void
305 Player::step_video_position (shared_ptr<DecodedVideo> video)
306 {
307         /* This is a bit of a hack; don't update _video_position if EYES_RIGHT is on its way */
308         if (video->eyes != EYES_LEFT) {
309                 /* This assumes that the video_frames_to_time conversion is exact
310                    so that there are no accumulated errors caused by rounding.
311                 */
312                 _video_position += _film->video_frames_to_time (1);
313         }
314 }
315
316 void
317 Player::emit_audio (weak_ptr<Piece> weak_piece, shared_ptr<DecodedAudio> audio)
318 {
319         shared_ptr<Piece> piece = weak_piece.lock ();
320         if (!piece) {
321                 return;
322         }
323
324         shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content);
325         assert (content);
326
327         /* Gain */
328         if (content->audio_gain() != 0) {
329                 shared_ptr<AudioBuffers> gain (new AudioBuffers (audio->data));
330                 gain->apply_gain (content->audio_gain ());
331                 audio->data = gain;
332         }
333
334         /* Remap channels */
335         shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->data->frames()));
336         dcp_mapped->make_silent ();
337         list<pair<int, libdcp::Channel> > map = content->audio_mapping().content_to_dcp ();
338         for (list<pair<int, libdcp::Channel> >::iterator i = map.begin(); i != map.end(); ++i) {
339                 if (i->first < audio->data->channels() && i->second < dcp_mapped->channels()) {
340                         dcp_mapped->accumulate_channel (audio->data.get(), i->first, i->second);
341                 }
342         }
343
344         audio->data = dcp_mapped;
345
346         /* Delay */
347         audio->dcp_time += content->audio_delay() * TIME_HZ / 1000;
348         if (audio->dcp_time < 0) {
349                 int const frames = - audio->dcp_time * _film->audio_frame_rate() / TIME_HZ;
350                 if (frames >= audio->data->frames ()) {
351                         return;
352                 }
353
354                 shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->data->channels(), audio->data->frames() - frames));
355                 trimmed->copy_from (audio->data.get(), audio->data->frames() - frames, frames, 0);
356
357                 audio->data = trimmed;
358                 audio->dcp_time = 0;
359         }
360
361         _audio_merger.push (audio->data, audio->dcp_time);
362 }
363
364 void
365 Player::flush ()
366 {
367         TimedAudioBuffers<DCPTime> tb = _audio_merger.flush ();
368         if (_audio && tb.audio) {
369                 Audio (tb.audio, tb.time);
370                 _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
371         }
372
373         while (_video && _video_position < _audio_position) {
374                 emit_black ();
375         }
376
377         while (_audio && _audio_position < _video_position) {
378                 emit_silence (_video_position - _audio_position);
379         }
380         
381 }
382
383 /** Seek so that the next pass() will yield (approximately) the requested frame.
384  *  Pass accurate = true to try harder to get close to the request.
385  *  @return true on error
386  */
387 void
388 Player::seek (DCPTime t, bool accurate)
389 {
390         if (!_have_valid_pieces) {
391                 setup_pieces ();
392         }
393
394         if (_pieces.empty ()) {
395                 return;
396         }
397
398         for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
399                 /* s is the offset of t from the start position of this content */
400                 DCPTime s = t - (*i)->content->position ();
401                 s = max (static_cast<DCPTime> (0), s);
402                 s = min ((*i)->content->length_after_trim(), s);
403
404                 /* Convert this to the content time */
405                 ContentTime ct = (s + (*i)->content->trim_start()) * (*i)->frc.speed_up;
406
407                 /* And seek the decoder */
408                 (*i)->decoder->seek (ct, accurate);
409         }
410
411         _video_position = time_round_up (t, TIME_HZ / _film->video_frame_rate());
412         _audio_position = time_round_up (t, TIME_HZ / _film->audio_frame_rate());
413
414         _audio_merger.clear (_audio_position);
415
416         if (!accurate) {
417                 /* We just did an inaccurate seek, so it's likely that the next thing seen
418                    out of pass() will be a fair distance from _{video,audio}_position.  Setting
419                    this flag stops pass() from trying to fix that: we assume that if it
420                    was an inaccurate seek then the caller does not care too much about
421                    inserting black/silence to keep the time tidy.
422                 */
423                 _just_did_inaccurate_seek = true;
424         }
425 }
426
427 void
428 Player::setup_pieces ()
429 {
430         list<shared_ptr<Piece> > old_pieces = _pieces;
431         _pieces.clear ();
432
433         ContentList content = _playlist->content ();
434
435         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
436
437                 shared_ptr<Decoder> decoder;
438                 optional<FrameRateChange> frc;
439
440                 shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
441                 if (fc) {
442                         decoder.reset (new FFmpegDecoder (_film, fc, _video, _audio));
443                         frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
444                 }
445                 
446                 shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
447                 if (ic) {
448                         /* See if we can re-use an old ImageDecoder */
449                         for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
450                                 shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
451                                 if (imd && imd->content() == ic) {
452                                         decoder = imd;
453                                 }
454                         }
455
456                         if (!decoder) {
457                                 decoder.reset (new ImageDecoder (_film, ic));
458                         }
459
460                         frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
461                 }
462
463                 shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
464                 if (sc) {
465                         decoder.reset (new SndfileDecoder (_film, sc));
466
467                         /* Working out the frc for this content is a bit tricky: what if it overlaps
468                            two pieces of video content with different frame rates?  For now, use
469                            the one with the best overlap.
470                         */
471
472                         DCPTime best_overlap_t = 0;
473                         shared_ptr<VideoContent> best_overlap;
474                         for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
475                                 shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
476                                 if (!vc) {
477                                         continue;
478                                 }
479
480                                 DCPTime const overlap = max (vc->position(), sc->position()) - min (vc->end(), sc->end());
481                                 if (overlap > best_overlap_t) {
482                                         best_overlap = vc;
483                                         best_overlap_t = overlap;
484                                 }
485                         }
486
487                         if (best_overlap) {
488                                 frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
489                         } else {
490                                 /* No video overlap; e.g. if the DCP is just audio */
491                                 frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
492                         }
493                 }
494
495                 ContentTime st = (*i)->trim_start() * frc->speed_up;
496                 decoder->seek (st, true);
497                 
498                 _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
499         }
500
501         _have_valid_pieces = true;
502
503         /* The Piece for the _last_incoming_video will no longer be valid */
504         _last_incoming_video.video.reset ();
505
506         _video_position = _audio_position = 0;
507 }
508
509 void
510 Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
511 {
512         shared_ptr<Content> c = w.lock ();
513         if (!c) {
514                 return;
515         }
516
517         if (
518                 property == ContentProperty::POSITION || property == ContentProperty::LENGTH ||
519                 property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END ||
520                 property == VideoContentProperty::VIDEO_FRAME_TYPE 
521                 ) {
522                 
523                 _have_valid_pieces = false;
524                 Changed (frequent);
525
526         } else if (property == SubtitleContentProperty::SUBTITLE_OFFSET || property == SubtitleContentProperty::SUBTITLE_SCALE) {
527
528                 update_subtitle ();
529                 Changed (frequent);
530
531         } else if (property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_RATIO) {
532                 
533                 Changed (frequent);
534
535         } else if (property == ContentProperty::PATH) {
536
537                 Changed (frequent);
538         }
539 }
540
541 void
542 Player::playlist_changed ()
543 {
544         _have_valid_pieces = false;
545         Changed (false);
546 }
547
548 void
549 Player::set_video_container_size (libdcp::Size s)
550 {
551         _video_container_size = s;
552
553         shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true));
554         im->make_black ();
555         
556         _black_frame.reset (
557                 new PlayerImage (
558                         im,
559                         Crop(),
560                         _video_container_size,
561                         _video_container_size,
562                         Scaler::from_id ("bicubic")
563                         )
564                 );
565 }
566
567 void
568 Player::emit_black ()
569 {
570 #ifdef DCPOMATIC_DEBUG
571         _last_video.reset ();
572 #endif
573
574         Video (_black_frame, EYES_BOTH, ColourConversion(), _last_emit_was_black, _video_position);
575         _video_position += _film->video_frames_to_time (1);
576         _last_emit_was_black = true;
577 }
578
579 void
580 Player::emit_silence (DCPTime most)
581 {
582         if (most == 0) {
583                 return;
584         }
585         
586         DCPTime t = min (most, TIME_HZ / 2);
587         shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), t * _film->audio_frame_rate() / TIME_HZ));
588         silence->make_silent ();
589         Audio (silence, _audio_position);
590         
591         _audio_position += t;
592 }
593
594 void
595 Player::film_changed (Film::Property p)
596 {
597         /* Here we should notice Film properties that affect our output, and
598            alert listeners that our output now would be different to how it was
599            last time we were run.
600         */
601
602         if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER) {
603                 Changed (false);
604         }
605 }
606
607 void
608 Player::update_subtitle ()
609 {
610         shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
611         if (!piece) {
612                 return;
613         }
614
615         if (!_in_subtitle.subtitle->image) {
616                 _out_subtitle.image.reset ();
617                 return;
618         }
619
620         shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
621         assert (sc);
622
623         dcpomatic::Rect<double> in_rect = _in_subtitle.subtitle->rect;
624         libdcp::Size scaled_size;
625
626         in_rect.y += sc->subtitle_offset ();
627
628         /* We will scale the subtitle up to fit _video_container_size, and also by the additional subtitle_scale */
629         scaled_size.width = in_rect.width * _video_container_size.width * sc->subtitle_scale ();
630         scaled_size.height = in_rect.height * _video_container_size.height * sc->subtitle_scale ();
631
632         /* Then we need a corrective translation, consisting of two parts:
633          *
634          * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
635          *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
636          *
637          * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
638          *     (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and
639          *     (height_before_subtitle_scale * (1 - subtitle_scale) / 2).
640          *
641          * Combining these two translations gives these expressions.
642          */
643         
644         _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
645         _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
646
647         _out_subtitle.image = _in_subtitle.subtitle->image->scale (
648                 scaled_size,
649                 Scaler::from_id ("bicubic"),
650                 PIX_FMT_RGBA,
651                 true
652                 );
653
654         _out_subtitle.from = _in_subtitle.subtitle->dcp_time;
655         _out_subtitle.to = _in_subtitle.subtitle->dcp_time_to;
656 }
657
658 /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
659  *  @return false if this could not be done.
660  */
661 bool
662 Player::repeat_last_video ()
663 {
664         if (!_last_incoming_video.video || !_have_valid_pieces) {
665                 return false;
666         }
667
668         emit_video (
669                 _last_incoming_video.weak_piece,
670                 _last_incoming_video.video
671                 );
672
673         return true;
674 }
675
676 void
677 Player::set_approximate_size ()
678 {
679         _approximate_size = true;
680 }
681                               
682
683 PlayerImage::PlayerImage (
684         shared_ptr<const Image> in,
685         Crop crop,
686         libdcp::Size inter_size,
687         libdcp::Size out_size,
688         Scaler const * scaler
689         )
690         : _in (in)
691         , _crop (crop)
692         , _inter_size (inter_size)
693         , _out_size (out_size)
694         , _scaler (scaler)
695 {
696
697 }
698
699 void
700 PlayerImage::set_subtitle (shared_ptr<const Image> image, Position<int> pos)
701 {
702         _subtitle_image = image;
703         _subtitle_position = pos;
704 }
705
706 shared_ptr<Image>
707 PlayerImage::image (AVPixelFormat format, bool aligned)
708 {
709         shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, format, aligned);
710         
711         Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
712
713         if (_subtitle_image) {
714                 out->alpha_blend (_subtitle_image, _subtitle_position);
715         }
716
717         return out;
718 }
719
720 void
721 PlayerStatistics::dump (shared_ptr<Log> log) const
722 {
723         log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat));
724         log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence));
725 }
726
727 PlayerStatistics const &
728 Player::statistics () const
729 {
730         return _statistics;
731 }