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