Fix horizontal positioning with .srt / burn-in (#488).
[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 {
78         _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
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::playlist_changed ()
230 {
231         _have_valid_pieces = false;
232         Changed (false);
233 }
234
235 void
236 Player::set_video_container_size (dcp::Size s)
237 {
238         _video_container_size = s;
239
240         _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true));
241         _black_image->make_black ();
242 }
243
244 void
245 Player::film_changed (Film::Property p)
246 {
247         /* Here we should notice Film properties that affect our output, and
248            alert listeners that our output now would be different to how it was
249            last time we were run.
250         */
251
252         if (p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
253                 Changed (false);
254         } else if (p == Film::AUDIO_PROCESSOR) {
255                 if (_film->audio_processor ()) {
256                         _audio_processor = _film->audio_processor()->clone (_film->audio_frame_rate ());
257                 }
258         }
259 }
260
261 list<PositionImage>
262 Player::transform_image_subtitles (list<ImageSubtitle> subs) const
263 {
264         list<PositionImage> all;
265         
266         for (list<ImageSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) {
267                 if (!i->image) {
268                         continue;
269                 }
270
271                 /* We will scale the subtitle up to fit _video_container_size */
272                 dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height);
273                 
274                 /* Then we need a corrective translation, consisting of two parts:
275                  *
276                  * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
277                  *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
278                  *
279                  * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
280                  *     (width_before_subtitle_scale * (1 - subtitle_x_scale) / 2) and
281                  *     (height_before_subtitle_scale * (1 - subtitle_y_scale) / 2).
282                  *
283                  * Combining these two translations gives these expressions.
284                  */
285
286                 all.push_back (
287                         PositionImage (
288                                 i->image->scale (
289                                         scaled_size,
290                                         dcp::YUV_TO_RGB_REC601,
291                                         i->image->pixel_format (),
292                                         true
293                                         ),
294                                 Position<int> (
295                                         rint (_video_container_size.width * i->rectangle.x),
296                                         rint (_video_container_size.height * i->rectangle.y)
297                                         )
298                                 )
299                         );
300         }
301
302         return all;
303 }
304
305 shared_ptr<PlayerVideo>
306 Player::black_player_video_frame (DCPTime time) const
307 {
308         return shared_ptr<PlayerVideo> (
309                 new PlayerVideo (
310                         shared_ptr<const ImageProxy> (new RawImageProxy (_black_image)),
311                         time,
312                         Crop (),
313                         optional<float> (),
314                         _video_container_size,
315                         _video_container_size,
316                         EYES_BOTH,
317                         PART_WHOLE,
318                         PresetColourConversion::all().front().conversion
319                 )
320         );
321 }
322
323 /** @return All PlayerVideos at the given time (there may be two frames for 3D) */
324 list<shared_ptr<PlayerVideo> >
325 Player::get_video (DCPTime time, bool accurate)
326 {
327         if (!_have_valid_pieces) {
328                 setup_pieces ();
329         }
330
331         list<shared_ptr<Piece> > ov = overlaps<VideoContent> (
332                 time,
333                 time + DCPTime::from_frames (1, _film->video_frame_rate ())
334                 );
335
336         list<shared_ptr<PlayerVideo> > pvf;
337
338         if (ov.empty ()) {
339                 /* No video content at this time */
340                 pvf.push_back (black_player_video_frame (time));
341         } else {
342                 /* Create a PlayerVideo from the content's video at this time */
343
344                 shared_ptr<Piece> piece = ov.back ();
345                 shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
346                 DCPOMATIC_ASSERT (decoder);
347                 shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
348                 DCPOMATIC_ASSERT (content);
349
350                 list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
351                 if (content_video.empty ()) {
352                         pvf.push_back (black_player_video_frame (time));
353                         return pvf;
354                 }
355                 
356                 dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
357
358                 for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) {
359                         pvf.push_back (
360                                 shared_ptr<PlayerVideo> (
361                                         new PlayerVideo (
362                                                 i->image,
363                                                 content_video_to_dcp (piece, i->frame),
364                                                 content->crop (),
365                                                 content->fade (i->frame),
366                                                 image_size,
367                                                 _video_container_size,
368                                                 i->eyes,
369                                                 i->part,
370                                                 content->colour_conversion ()
371                                                 )
372                                         )
373                                 );
374                 }
375         }
376
377         /* Add subtitles (for possible burn-in) to whatever PlayerVideos we got */
378
379         PlayerSubtitles ps = get_subtitles (time, DCPTime::from_frames (1, _film->video_frame_rate ()), false);
380
381         list<PositionImage> sub_images;
382
383         /* Image subtitles */
384         list<PositionImage> c = transform_image_subtitles (ps.image);
385         copy (c.begin(), c.end(), back_inserter (sub_images));
386
387         /* Text subtitles (rendered to an image) */
388         if (!ps.text.empty ()) {
389                 list<PositionImage> s = render_subtitles (ps.text, _video_container_size);
390                 copy (s.begin (), s.end (), back_inserter (sub_images));
391         }
392
393         if (!sub_images.empty ()) {
394                 for (list<shared_ptr<PlayerVideo> >::const_iterator i = pvf.begin(); i != pvf.end(); ++i) {
395                         (*i)->set_subtitle (merge (sub_images));
396                 }
397         }       
398                 
399         return pvf;
400 }
401
402 shared_ptr<AudioBuffers>
403 Player::get_audio (DCPTime time, DCPTime length, bool accurate)
404 {
405         if (!_have_valid_pieces) {
406                 setup_pieces ();
407         }
408
409         Frame const length_frames = length.frames (_film->audio_frame_rate ());
410
411         shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames));
412         audio->make_silent ();
413         
414         list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length);
415         if (ov.empty ()) {
416                 return audio;
417         }
418
419         for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) {
420
421                 shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content);
422                 DCPOMATIC_ASSERT (content);
423                 shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
424                 DCPOMATIC_ASSERT (decoder);
425
426                 /* The time that we should request from the content */
427                 DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0);
428                 Frame request_frames = length_frames;
429                 DCPTime offset;
430                 if (request < DCPTime ()) {
431                         /* We went off the start of the content, so we will need to offset
432                            the stuff we get back.
433                         */
434                         offset = -request;
435                         request_frames += request.frames (_film->audio_frame_rate ());
436                         if (request_frames < 0) {
437                                 request_frames = 0;
438                         }
439                         request = DCPTime ();
440                 }
441
442                 Frame const content_frame = dcp_to_content_audio (*i, request);
443
444                 BOOST_FOREACH (AudioStreamPtr j, content->audio_streams ()) {
445                         
446                         /* Audio from this piece's decoder stream (which might be more or less than what we asked for) */
447                         ContentAudio all = decoder->get_audio (j, content_frame, request_frames, accurate);
448
449                         /* Gain */
450                         if (content->audio_gain() != 0) {
451                                 shared_ptr<AudioBuffers> gain (new AudioBuffers (all.audio));
452                                 gain->apply_gain (content->audio_gain ());
453                                 all.audio = gain;
454                         }
455
456                         /* Remap channels */
457                         shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all.audio->frames()));
458                         dcp_mapped->make_silent ();
459                         AudioMapping map = j->mapping ();
460                         for (int i = 0; i < map.input_channels(); ++i) {
461                                 for (int j = 0; j < _film->audio_channels(); ++j) {
462                                         if (map.get (i, j) > 0) {
463                                                 dcp_mapped->accumulate_channel (
464                                                         all.audio.get(),
465                                                         i,
466                                                         j,
467                                                         map.get (i, j)
468                                                         );
469                                         }
470                                 }
471                         }
472
473                         if (_audio_processor) {
474                                 dcp_mapped = _audio_processor->run (dcp_mapped);
475                         }
476                 
477                         all.audio = dcp_mapped;
478
479                         audio->accumulate_frames (
480                                 all.audio.get(),
481                                 content_frame - all.frame,
482                                 offset.frames (_film->audio_frame_rate()),
483                                 min (Frame (all.audio->frames()), request_frames)
484                                 );
485                 }
486         }
487
488         return audio;
489 }
490
491 Frame
492 Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
493 {
494         /* s is the offset of t from the start position of this content */
495         DCPTime s = t - piece->content->position ();
496         s = DCPTime (max (DCPTime::Type (0), s.get ()));
497         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
498
499         /* Convert this to the content frame */
500         return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) / piece->frc.factor ();
501 }
502
503 DCPTime
504 Player::content_video_to_dcp (shared_ptr<const Piece> piece, Frame f) const
505 {
506         DCPTime t = DCPTime::from_frames (f * piece->frc.factor (), _film->video_frame_rate()) - piece->content->trim_start () + piece->content->position ();
507         if (t < DCPTime ()) {
508                 t = DCPTime ();
509         }
510
511         return t;
512 }
513
514 Frame
515 Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const
516 {
517         /* s is the offset of t from the start position of this content */
518         DCPTime s = t - piece->content->position ();
519         s = DCPTime (max (DCPTime::Type (0), s.get ()));
520         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
521
522         /* Convert this to the content frame */
523         return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate());
524 }
525
526 ContentTime
527 Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const
528 {
529         /* s is the offset of t from the start position of this content */
530         DCPTime s = t - piece->content->position ();
531         s = DCPTime (max (DCPTime::Type (0), s.get ()));
532         s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
533
534         return ContentTime (s + piece->content->trim_start(), piece->frc);
535 }
536
537 void
538 PlayerStatistics::dump (shared_ptr<Log> log) const
539 {
540         log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL);
541         log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL);
542 }
543
544 PlayerStatistics const &
545 Player::statistics () const
546 {
547         return _statistics;
548 }
549
550 PlayerSubtitles
551 Player::get_subtitles (DCPTime time, DCPTime length, bool starting)
552 {
553         list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (time, time + length);
554
555         PlayerSubtitles ps (time, length);
556
557         for (list<shared_ptr<Piece> >::const_iterator j = subs.begin(); j != subs.end(); ++j) {
558                 shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*j)->content);
559                 if (!subtitle_content->use_subtitles ()) {
560                         continue;
561                 }
562
563                 shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*j)->decoder);
564                 ContentTime const from = dcp_to_content_subtitle (*j, time);
565                 /* XXX: this video_frame_rate() should be the rate that the subtitle content has been prepared for */
566                 ContentTime const to = from + ContentTime::from_frames (1, _film->video_frame_rate ());
567
568                 list<ContentImageSubtitle> image = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to), starting);
569                 for (list<ContentImageSubtitle>::iterator i = image.begin(); i != image.end(); ++i) {
570                         
571                         /* Apply content's subtitle offsets */
572                         i->sub.rectangle.x += subtitle_content->subtitle_x_offset ();
573                         i->sub.rectangle.y += subtitle_content->subtitle_y_offset ();
574
575                         /* Apply content's subtitle scale */
576                         i->sub.rectangle.width *= subtitle_content->subtitle_x_scale ();
577                         i->sub.rectangle.height *= subtitle_content->subtitle_y_scale ();
578
579                         /* Apply a corrective translation to keep the subtitle centred after that scale */
580                         i->sub.rectangle.x -= i->sub.rectangle.width * (subtitle_content->subtitle_x_scale() - 1);
581                         i->sub.rectangle.y -= i->sub.rectangle.height * (subtitle_content->subtitle_y_scale() - 1);
582                         
583                         ps.image.push_back (i->sub);
584                 }
585
586                 list<ContentTextSubtitle> text = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to), starting);
587                 BOOST_FOREACH (ContentTextSubtitle& ts, text) {
588                         BOOST_FOREACH (dcp::SubtitleString& s, ts.subs) {
589                                 s.set_h_position (s.h_position() + subtitle_content->subtitle_x_offset ());
590                                 s.set_v_position (s.v_position() + subtitle_content->subtitle_y_offset ());
591                                 float const xs = subtitle_content->subtitle_x_scale();
592                                 float const ys = subtitle_content->subtitle_y_scale();
593                                 float const average = s.size() * (xs + ys) / 2;
594                                 s.set_size (average);
595                                 if (fabs (1.0 - xs / ys) > dcp::ASPECT_ADJUST_EPSILON) {
596                                         s.set_aspect_adjust (xs / ys);
597                                 }
598                                 ps.text.push_back (s);
599                         }
600                 }
601         }
602
603         return ps;
604 }
605
606 list<shared_ptr<Font> >
607 Player::get_subtitle_fonts ()
608 {
609         if (!_have_valid_pieces) {
610                 setup_pieces ();
611         }
612
613         list<shared_ptr<Font> > fonts;
614         BOOST_FOREACH (shared_ptr<Piece>& p, _pieces) {
615                 shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (p->content);
616                 if (sc) {
617                         /* XXX: things may go wrong if there are duplicate font IDs
618                            with different font files.
619                         */
620                         list<shared_ptr<Font> > f = sc->fonts ();
621                         copy (f.begin(), f.end(), back_inserter (fonts));
622                 }
623         }
624
625         return fonts;
626 }
627
628 /** Set this player never to produce any video data */
629 void
630 Player::set_ignore_video ()
631 {
632         _ignore_video = true;
633 }