Use Film::Changed instead of Playlist::Changed in Player.
[dcpomatic.git] / src / lib / player.cc
1 /*
2     Copyright (C) 2013-2015 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 "player.h"
21 #include "film.h"
22 #include "ffmpeg_decoder.h"
23 #include "audio_buffers.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 "subrip_decoder.h"
31 #include "subrip_content.h"
32 #include "dcp_content.h"
33 #include "playlist.h"
34 #include "job.h"
35 #include "image.h"
36 #include "raw_image_proxy.h"
37 #include "ratio.h"
38 #include "log.h"
39 #include "render_subtitles.h"
40 #include "config.h"
41 #include "content_video.h"
42 #include "player_video.h"
43 #include "frame_rate_change.h"
44 #include "dcp_content.h"
45 #include "dcp_decoder.h"
46 #include "dcp_subtitle_content.h"
47 #include "dcp_subtitle_decoder.h"
48 #include "audio_processor.h"
49 #include <boost/foreach.hpp>
50 #include <stdint.h>
51 #include <algorithm>
52
53 #include "i18n.h"
54
55 #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
56
57 using std::list;
58 using std::cout;
59 using std::min;
60 using std::max;
61 using std::min;
62 using std::vector;
63 using std::pair;
64 using std::map;
65 using std::make_pair;
66 using std::copy;
67 using boost::shared_ptr;
68 using boost::weak_ptr;
69 using boost::dynamic_pointer_cast;
70 using boost::optional;
71
72 Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
73         : _film (f)
74         , _playlist (p)
75         , _have_valid_pieces (false)
76         , _ignore_video (false)
77         , _burn_subtitles (f->burn_subtitles ())
78 {
79         _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
80         _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1));
81         set_video_container_size (_film->frame_size ());
82
83         film_changed (Film::AUDIO_PROCESSOR);
84 }
85
86 void
87 Player::setup_pieces ()
88 {
89         list<shared_ptr<Piece> > old_pieces = _pieces;
90         _pieces.clear ();
91
92         ContentList content = _playlist->content ();
93
94         for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
95
96                 if (!(*i)->paths_valid ()) {
97                         continue;
98                 }
99                 
100                 shared_ptr<Decoder> decoder;
101                 optional<FrameRateChange> frc;
102
103                 /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
104                 DCPTime best_overlap_t;
105                 shared_ptr<VideoContent> best_overlap;
106                 for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
107                         shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
108                         if (!vc) {
109                                 continue;
110                         }
111                         
112                         DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
113                         if (overlap > best_overlap_t) {
114                                 best_overlap = vc;
115                                 best_overlap_t = overlap;
116                         }
117                 }
118
119                 optional<FrameRateChange> best_overlap_frc;
120                 if (best_overlap) {
121                         best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
122                 } else {
123                         /* No video overlap; e.g. if the DCP is just audio */
124                         best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
125                 }
126
127                 /* FFmpeg */
128                 shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
129                 if (fc) {
130                         decoder.reset (new FFmpegDecoder (fc, _film->log()));
131                         frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
132                 }
133
134                 shared_ptr<const DCPContent> dc = dynamic_pointer_cast<const DCPContent> (*i);
135                 if (dc) {
136                         decoder.reset (new DCPDecoder (dc));
137                         frc = FrameRateChange (dc->video_frame_rate(), _film->video_frame_rate());
138                 }
139
140                 /* ImageContent */
141                 shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
142                 if (ic) {
143                         /* See if we can re-use an old ImageDecoder */
144                         for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
145                                 shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
146                                 if (imd && imd->content() == ic) {
147                                         decoder = imd;
148                                 }
149                         }
150
151                         if (!decoder) {
152                                 decoder.reset (new ImageDecoder (ic));
153                         }
154
155                         frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
156                 }
157
158                 /* SndfileContent */
159                 shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
160                 if (sc) {
161                         decoder.reset (new SndfileDecoder (sc));
162                         frc = best_overlap_frc;
163                 }
164
165                 /* SubRipContent */
166                 shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
167                 if (rc) {
168                         decoder.reset (new SubRipDecoder (rc));
169                         frc = best_overlap_frc;
170                 }
171
172                 /* DCPSubtitleContent */
173                 shared_ptr<const DCPSubtitleContent> dsc = dynamic_pointer_cast<const DCPSubtitleContent> (*i);
174                 if (dsc) {
175                         decoder.reset (new DCPSubtitleDecoder (dsc));
176                         frc = best_overlap_frc;
177                 }
178
179                 shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> (decoder);
180                 if (vd && _ignore_video) {
181                         vd->set_ignore_video ();
182                 }
183
184                 _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
185         }
186
187         _have_valid_pieces = true;
188 }
189
190 void
191 Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
192 {
193         shared_ptr<Content> c = w.lock ();
194         if (!c) {
195                 return;
196         }
197
198         if (
199                 property == ContentProperty::POSITION ||
200                 property == ContentProperty::LENGTH ||
201                 property == ContentProperty::TRIM_START ||
202                 property == ContentProperty::TRIM_END ||
203                 property == ContentProperty::PATH ||
204                 property == VideoContentProperty::VIDEO_FRAME_TYPE ||
205                 property == DCPContentProperty::CAN_BE_PLAYED
206                 ) {
207                 
208                 _have_valid_pieces = false;
209                 Changed (frequent);
210
211         } else if (
212                 property == SubtitleContentProperty::USE_SUBTITLES ||
213                 property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
214                 property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
215                 property == SubtitleContentProperty::SUBTITLE_X_SCALE ||
216                 property == SubtitleContentProperty::SUBTITLE_Y_SCALE ||
217                 property == VideoContentProperty::VIDEO_CROP ||
218                 property == VideoContentProperty::VIDEO_SCALE ||
219                 property == VideoContentProperty::VIDEO_FRAME_RATE ||
220                 property == VideoContentProperty::VIDEO_FADE_IN ||
221                 property == VideoContentProperty::VIDEO_FADE_OUT
222                 ) {
223                 
224                 Changed (frequent);
225         }
226 }
227
228 void
229 Player::set_video_container_size (dcp::Size s)
230 {
231         _video_container_size = s;
232
233         _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true));
234         _black_image->make_black ();
235 }
236
237 void
238 Player::film_changed (Film::Property p)
239 {
240         /* Here we should notice Film properties that affect our output, and
241            alert listeners that our output now would be different to how it was
242            last time we were run.
243         */
244
245         if (p == Film::CONTENT) {
246                 _have_valid_pieces = false;
247                 Changed (false);
248         } else if (p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
249                 Changed (false);
250         } else if (p == Film::AUDIO_PROCESSOR) {
251                 if (_film->audio_processor ()) {
252                         _audio_processor = _film->audio_processor()->clone (_film->audio_frame_rate ());
253                 }
254         }
255 }
256
257 list<PositionImage>
258 Player::transform_image_subtitles (list<ImageSubtitle> subs) const
259 {
260         list<PositionImage> all;
261         
262         for (list<ImageSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) {
263                 if (!i->image) {
264                         continue;
265                 }
266
267                 /* We will scale the subtitle up to fit _video_container_size */
268                 dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height);
269                 
270                 /* Then we need a corrective translation, consisting of two parts:
271                  *
272                  * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
273                  *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
274                  *
275                  * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
276                  *     (width_before_subtitle_scale * (1 - subtitle_x_scale) / 2) and
277                  *     (height_before_subtitle_scale * (1 - subtitle_y_scale) / 2).
278                  *
279                  * Combining these two translations gives these expressions.
280                  */
281
282                 all.push_back (
283                         PositionImage (
284                                 i->image->scale (
285                                         scaled_size,
286                                         dcp::YUV_TO_RGB_REC601,
287                                         i->image->pixel_format (),
288                                         true
289                                         ),
290                                 Position<int> (
291                                         rint (_video_container_size.width * i->rectangle.x),
292                                         rint (_video_container_size.height * i->rectangle.y)
293                                         )
294                                 )
295                         );
296         }
297
298         return all;
299 }
300
301 shared_ptr<PlayerVideo>
302 Player::black_player_video_frame (DCPTime time) const
303 {
304         return shared_ptr<PlayerVideo> (
305                 new PlayerVideo (
306                         shared_ptr<const ImageProxy> (new RawImageProxy (_black_image)),
307                         time,
308                         Crop (),
309                         optional<float> (),
310                         _video_container_size,
311                         _video_container_size,
312                         EYES_BOTH,
313                         PART_WHOLE,
314                         PresetColourConversion::all().front().conversion
315                 )
316         );
317 }
318
319 /** @return All PlayerVideos at the given time (there may be two frames for 3D) */
320 list<shared_ptr<PlayerVideo> >
321 Player::get_video (DCPTime time, bool accurate)
322 {
323         if (!_have_valid_pieces) {
324                 setup_pieces ();
325         }
326
327         list<shared_ptr<Piece> > ov = overlaps<VideoContent> (
328                 time,
329                 time + DCPTime::from_frames (1, _film->video_frame_rate ())
330                 );
331
332         list<shared_ptr<PlayerVideo> > pvf;
333
334         if (ov.empty ()) {
335                 /* No video content at this time */
336                 pvf.push_back (black_player_video_frame (time));
337         } else {
338                 /* Create a PlayerVideo from the content's video at this time */
339
340                 shared_ptr<Piece> piece = ov.back ();
341                 shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
342                 DCPOMATIC_ASSERT (decoder);
343                 shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
344                 DCPOMATIC_ASSERT (content);
345
346                 list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
347                 if (content_video.empty ()) {
348                         pvf.push_back (black_player_video_frame (time));
349                         return pvf;
350                 }
351                 
352                 dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
353
354                 for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) {
355                         pvf.push_back (
356                                 shared_ptr<PlayerVideo> (
357                                         new PlayerVideo (
358                                                 i->image,
359                                                 content_video_to_dcp (piece, i->frame),
360                                                 content->crop (),
361                                                 content->fade (i->frame),
362                                                 image_size,
363                                                 _video_container_size,
364                                                 i->eyes,
365                                                 i->part,
366                                                 content->colour_conversion ()
367                                                 )
368                                         )
369                                 );
370                 }
371         }
372
373         /* Add subtitles (for possible burn-in) to whatever PlayerVideos we got */
374
375         PlayerSubtitles ps = get_subtitles (time, DCPTime::from_frames (1, _film->video_frame_rate ()), false);
376
377         list<PositionImage> sub_images;
378
379         /* Image subtitles */
380         list<PositionImage> c = transform_image_subtitles (ps.image);
381         copy (c.begin(), c.end(), back_inserter (sub_images));
382
383         /* Text subtitles (rendered to an image) */
384         if (_burn_subtitles && !ps.text.empty ()) {
385                 list<PositionImage> s = render_subtitles (ps.text, _video_container_size);
386                 copy (s.begin (), s.end (), back_inserter (sub_images));
387         }
388
389         if (!sub_images.empty ()) {
390                 for (list<shared_ptr<PlayerVideo> >::const_iterator i = pvf.begin(); i != pvf.end(); ++i) {
391                         (*i)->set_subtitle (merge (sub_images));
392                 }
393         }       
394                 
395         return pvf;
396 }
397
398 shared_ptr<AudioBuffers>
399 Player::get_audio (DCPTime time, DCPTime length, bool accurate)
400 {
401         if (!_have_valid_pieces) {
402                 setup_pieces ();
403         }
404
405         Frame const length_frames = length.frames (_film->audio_frame_rate ());
406
407         shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames));
408         audio->make_silent ();
409         
410         list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length);
411         if (ov.empty ()) {
412                 return audio;
413         }
414
415         for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) {
416
417                 shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content);
418                 DCPOMATIC_ASSERT (content);
419                 shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
420                 DCPOMATIC_ASSERT (decoder);
421
422                 /* The time that we should request from the content */
423                 DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0);
424                 Frame request_frames = length_frames;
425                 DCPTime offset;
426                 if (request < DCPTime ()) {
427                         /* We went off the start of the content, so we will need to offset
428                            the stuff we get back.
429                         */
430                         offset = -request;
431                         request_frames += request.frames (_film->audio_frame_rate ());
432                         if (request_frames < 0) {
433                                 request_frames = 0;
434                         }
435                         request = DCPTime ();
436                 }
437
438                 Frame const content_frame = dcp_to_content_audio (*i, request);
439
440                 BOOST_FOREACH (AudioStreamPtr j, content->audio_streams ()) {
441                         
442                         /* Audio from this piece's decoder stream (which might be more or less than what we asked for) */
443                         ContentAudio all = decoder->get_audio (j, content_frame, request_frames, accurate);
444
445                         /* Gain */
446                         if (content->audio_gain() != 0) {
447                                 shared_ptr<AudioBuffers> gain (new AudioBuffers (all.audio));
448                                 gain->apply_gain (content->audio_gain ());
449                                 all.audio = gain;
450                         }
451
452                         /* Remap channels */
453                         shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all.audio->frames()));
454                         dcp_mapped->make_silent ();
455                         AudioMapping map = j->mapping ();
456                         for (int i = 0; i < map.input_channels(); ++i) {
457                                 for (int j = 0; j < _film->audio_channels(); ++j) {
458                                         if (map.get (i, j) > 0) {
459                                                 dcp_mapped->accumulate_channel (
460                                                         all.audio.get(),
461                                                         i,
462                                                         j,
463                                                         map.get (i, j)
464                                                         );
465                                         }
466                                 }
467                         }
468
469                         if (_audio_processor) {
470                                 dcp_mapped = _audio_processor->run (dcp_mapped);
471                         }
472                 
473                         all.audio = dcp_mapped;
474
475                         audio->accumulate_frames (
476                                 all.audio.get(),
477                                 content_frame - all.frame,
478                                 offset.frames (_film->audio_frame_rate()),
479                                 min (Frame (all.audio->frames()), request_frames)
480                                 );
481                 }
482         }
483
484         return audio;
485 }
486
487 Frame
488 Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
489 {
490         /* s is the offset of t from the start position of this content */
491         DCPTime s = t - piece->content->position ();
492         s = DCPTime (max (DCPTime::Type (0), s.get ()));
493         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
494
495         /* Convert this to the content frame */
496         return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) / piece->frc.factor ();
497 }
498
499 DCPTime
500 Player::content_video_to_dcp (shared_ptr<const Piece> piece, Frame f) const
501 {
502         DCPTime t = DCPTime::from_frames (f * piece->frc.factor (), _film->video_frame_rate()) - piece->content->trim_start () + piece->content->position ();
503         if (t < DCPTime ()) {
504                 t = DCPTime ();
505         }
506
507         return t;
508 }
509
510 Frame
511 Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const
512 {
513         /* s is the offset of t from the start position of this content */
514         DCPTime s = t - piece->content->position ();
515         s = DCPTime (max (DCPTime::Type (0), s.get ()));
516         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
517
518         /* Convert this to the content frame */
519         return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate());
520 }
521
522 ContentTime
523 Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const
524 {
525         /* s is the offset of t from the start position of this content */
526         DCPTime s = t - piece->content->position ();
527         s = DCPTime (max (DCPTime::Type (0), s.get ()));
528         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
529
530         return ContentTime (s + piece->content->trim_start(), piece->frc);
531 }
532
533 void
534 PlayerStatistics::dump (shared_ptr<Log> log) const
535 {
536         log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL);
537         log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL);
538 }
539
540 PlayerStatistics const &
541 Player::statistics () const
542 {
543         return _statistics;
544 }
545
546 PlayerSubtitles
547 Player::get_subtitles (DCPTime time, DCPTime length, bool starting)
548 {
549         list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (time, time + length);
550
551         PlayerSubtitles ps (time, length);
552
553         for (list<shared_ptr<Piece> >::const_iterator j = subs.begin(); j != subs.end(); ++j) {
554                 shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*j)->content);
555                 if (!subtitle_content->use_subtitles ()) {
556                         continue;
557                 }
558
559                 shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*j)->decoder);
560                 ContentTime const from = dcp_to_content_subtitle (*j, time);
561                 /* XXX: this video_frame_rate() should be the rate that the subtitle content has been prepared for */
562                 ContentTime const to = from + ContentTime::from_frames (1, _film->video_frame_rate ());
563
564                 list<ContentImageSubtitle> image = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to), starting);
565                 for (list<ContentImageSubtitle>::iterator i = image.begin(); i != image.end(); ++i) {
566                         
567                         /* Apply content's subtitle offsets */
568                         i->sub.rectangle.x += subtitle_content->subtitle_x_offset ();
569                         i->sub.rectangle.y += subtitle_content->subtitle_y_offset ();
570
571                         /* Apply content's subtitle scale */
572                         i->sub.rectangle.width *= subtitle_content->subtitle_x_scale ();
573                         i->sub.rectangle.height *= subtitle_content->subtitle_y_scale ();
574
575                         /* Apply a corrective translation to keep the subtitle centred after that scale */
576                         i->sub.rectangle.x -= i->sub.rectangle.width * (subtitle_content->subtitle_x_scale() - 1);
577                         i->sub.rectangle.y -= i->sub.rectangle.height * (subtitle_content->subtitle_y_scale() - 1);
578                         
579                         ps.image.push_back (i->sub);
580                 }
581
582                 list<ContentTextSubtitle> text = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to), starting);
583                 BOOST_FOREACH (ContentTextSubtitle& ts, text) {
584                         BOOST_FOREACH (dcp::SubtitleString& s, ts.subs) {
585                                 s.set_h_position (s.h_position() + subtitle_content->subtitle_x_offset ());
586                                 s.set_v_position (s.v_position() + subtitle_content->subtitle_y_offset ());
587                                 float const xs = subtitle_content->subtitle_x_scale();
588                                 float const ys = subtitle_content->subtitle_y_scale();
589                                 float const average = s.size() * (xs + ys) / 2;
590                                 s.set_size (average);
591                                 if (fabs (1.0 - xs / ys) > dcp::ASPECT_ADJUST_EPSILON) {
592                                         s.set_aspect_adjust (xs / ys);
593                                 }
594                                 ps.text.push_back (s);
595                         }
596                 }
597         }
598
599         return ps;
600 }
601
602 list<shared_ptr<Font> >
603 Player::get_subtitle_fonts ()
604 {
605         if (!_have_valid_pieces) {
606                 setup_pieces ();
607         }
608
609         list<shared_ptr<Font> > fonts;
610         BOOST_FOREACH (shared_ptr<Piece>& p, _pieces) {
611                 shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (p->content);
612                 if (sc) {
613                         /* XXX: things may go wrong if there are duplicate font IDs
614                            with different font files.
615                         */
616                         list<shared_ptr<Font> > f = sc->fonts ();
617                         copy (f.begin(), f.end(), back_inserter (fonts));
618                 }
619         }
620
621         return fonts;
622 }
623
624 /** Set this player never to produce any video data */
625 void
626 Player::set_ignore_video ()
627 {
628         _ignore_video = true;
629 }
630
631 /** Set whether or not this player should burn text subtitles into the image.
632  *  @param burn true to burn subtitles, false to not.
633  */
634 void
635 Player::set_burn_subtitles (bool burn)
636 {
637         _burn_subtitles = burn;
638 }