Merge master and multifarious hackery.
[dcpomatic.git] / src / lib / film.cc
1 /* -*- c-basic-offset: 8; default-tab-width: 8; -*- */
2
3 /*
4     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
5
6     This program is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with this program; if not, write to the Free Software
18     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
20 */
21
22 #include <stdexcept>
23 #include <iostream>
24 #include <algorithm>
25 #include <fstream>
26 #include <cstdlib>
27 #include <sstream>
28 #include <iomanip>
29 #include <unistd.h>
30 #include <boost/filesystem.hpp>
31 #include <boost/algorithm/string.hpp>
32 #include <boost/lexical_cast.hpp>
33 #include <boost/date_time.hpp>
34 #include <libxml++/libxml++.h>
35 #include <libcxml/cxml.h>
36 #include "film.h"
37 #include "container.h"
38 #include "job.h"
39 #include "filter.h"
40 #include "util.h"
41 #include "job_manager.h"
42 #include "ab_transcode_job.h"
43 #include "transcode_job.h"
44 #include "scp_dcp_job.h"
45 #include "log.h"
46 #include "exceptions.h"
47 #include "examine_content_job.h"
48 #include "scaler.h"
49 #include "config.h"
50 #include "version.h"
51 #include "ui_signaller.h"
52 #include "analyse_audio_job.h"
53 #include "playlist.h"
54 #include "player.h"
55 #include "ffmpeg_content.h"
56 #include "imagemagick_content.h"
57 #include "sndfile_content.h"
58 #include "dcp_content_type.h"
59
60 #include "i18n.h"
61
62 using std::string;
63 using std::stringstream;
64 using std::multimap;
65 using std::pair;
66 using std::map;
67 using std::vector;
68 using std::ifstream;
69 using std::ofstream;
70 using std::setfill;
71 using std::min;
72 using std::make_pair;
73 using std::endl;
74 using std::cout;
75 using std::list;
76 using boost::shared_ptr;
77 using boost::lexical_cast;
78 using boost::to_upper_copy;
79 using boost::ends_with;
80 using boost::starts_with;
81 using boost::optional;
82 using libdcp::Size;
83
84 int const Film::state_version = 4;
85
86 /** Construct a Film object in a given directory.
87  *
88  *  @param d Film directory.
89  */
90
91 Film::Film (string d)
92         : _playlist (new Playlist)
93         , _use_dci_name (true)
94         , _dcp_content_type (Config::instance()->default_dcp_content_type ())
95         , _container (Config::instance()->default_container ())
96         , _scaler (Scaler::from_id ("bicubic"))
97         , _ab (false)
98         , _with_subtitles (false)
99         , _subtitle_offset (0)
100         , _subtitle_scale (1)
101         , _colour_lut (0)
102         , _j2k_bandwidth (200000000)
103         , _dci_metadata (Config::instance()->default_dci_metadata ())
104         , _dcp_video_frame_rate (0)
105         , _dcp_audio_channels (MAX_AUDIO_CHANNELS)
106         , _dirty (false)
107 {
108         set_dci_date_today ();
109
110         _playlist->Changed.connect (bind (&Film::playlist_changed, this));
111         _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2));
112         
113         /* Make state.directory a complete path without ..s (where possible)
114            (Code swiped from Adam Bowen on stackoverflow)
115         */
116         
117         boost::filesystem::path p (boost::filesystem::system_complete (d));
118         boost::filesystem::path result;
119         for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
120                 if (*i == "..") {
121                         if (boost::filesystem::is_symlink (result) || result.filename() == "..") {
122                                 result /= *i;
123                         } else {
124                                 result = result.parent_path ();
125                         }
126                 } else if (*i != ".") {
127                         result /= *i;
128                 }
129         }
130
131         set_directory (result.string ());
132         _log.reset (new FileLog (file ("log")));
133 }
134
135 Film::Film (Film const & o)
136         : boost::enable_shared_from_this<Film> (o)
137         /* note: the copied film shares the original's log */
138         , _log               (o._log)
139         , _playlist          (new Playlist (o._playlist))
140         , _directory         (o._directory)
141         , _name              (o._name)
142         , _use_dci_name      (o._use_dci_name)
143         , _dcp_content_type  (o._dcp_content_type)
144         , _container         (o._container)
145         , _scaler            (o._scaler)
146         , _ab                (o._ab)
147         , _with_subtitles    (o._with_subtitles)
148         , _subtitle_offset   (o._subtitle_offset)
149         , _subtitle_scale    (o._subtitle_scale)
150         , _colour_lut        (o._colour_lut)
151         , _j2k_bandwidth     (o._j2k_bandwidth)
152         , _dci_metadata      (o._dci_metadata)
153         , _dcp_video_frame_rate (o._dcp_video_frame_rate)
154         , _dci_date          (o._dci_date)
155         , _dirty             (o._dirty)
156 {
157         _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2));
158 }
159
160 string
161 Film::video_state_identifier () const
162 {
163         assert (container ());
164         LocaleGuard lg;
165
166         stringstream s;
167         s << container()->id()
168           << "_" << _playlist->video_digest()
169           << "_" << _dcp_video_frame_rate
170           << "_" << scaler()->id()
171           << "_" << j2k_bandwidth()
172           << "_" << lexical_cast<int> (colour_lut());
173
174         if (ab()) {
175                 pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters());
176                 s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second;
177         }
178
179         return s.str ();
180 }
181           
182 /** @return The path to the directory to write video frame info files to */
183 string
184 Film::info_dir () const
185 {
186         boost::filesystem::path p;
187         p /= "info";
188         p /= video_state_identifier ();
189         return dir (p.string());
190 }
191
192 string
193 Film::internal_video_mxf_dir () const
194 {
195         boost::filesystem::path p;
196         return dir ("video");
197 }
198
199 string
200 Film::internal_video_mxf_filename () const
201 {
202         return video_state_identifier() + ".mxf";
203 }
204
205 string
206 Film::dcp_video_mxf_filename () const
207 {
208         return filename_safe_name() + "_video.mxf";
209 }
210
211 string
212 Film::dcp_audio_mxf_filename () const
213 {
214         return filename_safe_name() + "_audio.mxf";
215 }
216
217 string
218 Film::filename_safe_name () const
219 {
220         string const n = name ();
221         string o;
222         for (size_t i = 0; i < n.length(); ++i) {
223                 if (isalnum (n[i])) {
224                         o += n[i];
225                 } else {
226                         o += "_";
227                 }
228         }
229
230         return o;
231 }
232
233 string
234 Film::audio_analysis_path () const
235 {
236         boost::filesystem::path p;
237         p /= "analysis";
238         p /= _playlist->audio_digest();
239         return file (p.string ());
240 }
241
242 /** Add suitable Jobs to the JobManager to create a DCP for this Film */
243 void
244 Film::make_dcp ()
245 {
246         set_dci_date_today ();
247         
248         if (dcp_name().find ("/") != string::npos) {
249                 throw BadSettingError (_("name"), _("cannot contain slashes"));
250         }
251         
252         log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
253
254         {
255                 char buffer[128];
256                 gethostname (buffer, sizeof (buffer));
257                 log()->log (String::compose ("Starting to make DCP on %1", buffer));
258         }
259         
260 //      log()->log (String::compose ("Content is %1; type %2", content_path(), (content_type() == STILL ? _("still") : _("video"))));
261 //      if (length()) {
262 //              log()->log (String::compose ("Content length %1", length().get()));
263 //      }
264 //      log()->log (String::compose ("Content digest %1", content_digest()));
265 //      log()->log (String::compose ("Content at %1 fps, DCP at %2 fps", source_frame_rate(), dcp_frame_rate()));
266         log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads()));
267         log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth()));
268 #ifdef DCPOMATIC_DEBUG
269         log()->log ("DCP-o-matic built in debug mode.");
270 #else
271         log()->log ("DCP-o-matic built in optimised mode.");
272 #endif
273 #ifdef LIBDCP_DEBUG
274         log()->log ("libdcp built in debug mode.");
275 #else
276         log()->log ("libdcp built in optimised mode.");
277 #endif
278         pair<string, int> const c = cpu_info ();
279         log()->log (String::compose ("CPU: %1, %2 processors", c.first, c.second));
280         
281         if (container() == 0) {
282                 throw MissingSettingError (_("container"));
283         }
284
285         if (_playlist->content().empty ()) {
286                 throw StringError (_("You must add some content to the DCP before creating it"));
287         }
288
289         if (dcp_content_type() == 0) {
290                 throw MissingSettingError (_("content type"));
291         }
292
293         if (name().empty()) {
294                 throw MissingSettingError (_("name"));
295         }
296
297         shared_ptr<Job> r;
298
299         if (ab()) {
300                 r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this())));
301         } else {
302                 r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
303         }
304 }
305
306 /** Start a job to analyse the audio in our Playlist */
307 void
308 Film::analyse_audio ()
309 {
310         if (_analyse_audio_job) {
311                 return;
312         }
313
314         _analyse_audio_job.reset (new AnalyseAudioJob (shared_from_this()));
315         _analyse_audio_job->Finished.connect (bind (&Film::analyse_audio_finished, this));
316         JobManager::instance()->add (_analyse_audio_job);
317 }
318
319 /** Start a job to examine a piece of content */
320 void
321 Film::examine_content (shared_ptr<Content> c)
322 {
323         shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
324         JobManager::instance()->add (j);
325 }
326
327 void
328 Film::analyse_audio_finished ()
329 {
330         ensure_ui_thread ();
331
332         if (_analyse_audio_job->finished_ok ()) {
333                 AudioAnalysisSucceeded ();
334         }
335         
336         _analyse_audio_job.reset ();
337 }
338
339 /** Start a job to send our DCP to the configured TMS */
340 void
341 Film::send_dcp_to_tms ()
342 {
343         shared_ptr<Job> j (new SCPDCPJob (shared_from_this()));
344         JobManager::instance()->add (j);
345 }
346
347 /** Count the number of frames that have been encoded for this film.
348  *  @return frame count.
349  */
350 int
351 Film::encoded_frames () const
352 {
353         if (container() == 0) {
354                 return 0;
355         }
356
357         int N = 0;
358         for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (info_dir ()); i != boost::filesystem::directory_iterator(); ++i) {
359                 ++N;
360                 boost::this_thread::interruption_point ();
361         }
362
363         return N;
364 }
365
366 /** Write state to our `metadata' file */
367 void
368 Film::write_metadata () const
369 {
370         if (!boost::filesystem::exists (directory())) {
371                 boost::filesystem::create_directory (directory());
372         }
373         
374         boost::mutex::scoped_lock lm (_state_mutex);
375         LocaleGuard lg;
376
377         boost::filesystem::create_directories (directory());
378
379         xmlpp::Document doc;
380         xmlpp::Element* root = doc.create_root_node ("Metadata");
381
382         root->add_child("Version")->add_child_text (lexical_cast<string> (state_version));
383         root->add_child("Name")->add_child_text (_name);
384         root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0");
385
386         if (_dcp_content_type) {
387                 root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ());
388         }
389
390         if (_container) {
391                 root->add_child("Container")->add_child_text (_container->id ());
392         }
393
394         root->add_child("Scaler")->add_child_text (_scaler->id ());
395         root->add_child("AB")->add_child_text (_ab ? "1" : "0");
396         root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
397         root->add_child("SubtitleOffset")->add_child_text (lexical_cast<string> (_subtitle_offset));
398         root->add_child("SubtitleScale")->add_child_text (lexical_cast<string> (_subtitle_scale));
399         root->add_child("ColourLUT")->add_child_text (lexical_cast<string> (_colour_lut));
400         root->add_child("J2KBandwidth")->add_child_text (lexical_cast<string> (_j2k_bandwidth));
401         _dci_metadata.as_xml (root->add_child ("DCIMetadata"));
402         root->add_child("DCPVideoFrameRate")->add_child_text (lexical_cast<string> (_dcp_video_frame_rate));
403         root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date));
404         root->add_child("DCPAudioChannels")->add_child_text (lexical_cast<string> (_dcp_audio_channels));
405         _playlist->as_xml (root->add_child ("Playlist"));
406
407         doc.write_to_file_formatted (file ("metadata.xml"));
408         
409         _dirty = false;
410 }
411
412 /** Read state from our metadata file */
413 void
414 Film::read_metadata ()
415 {
416         boost::mutex::scoped_lock lm (_state_mutex);
417         LocaleGuard lg;
418
419         if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
420                 throw StringError (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
421         }
422
423         cxml::File f (file ("metadata.xml"), "Metadata");
424         
425         _name = f.string_child ("Name");
426         _use_dci_name = f.bool_child ("UseDCIName");
427
428         {
429                 optional<string> c = f.optional_string_child ("DCPContentType");
430                 if (c) {
431                         _dcp_content_type = DCPContentType::from_dci_name (c.get ());
432                 }
433         }
434
435         {
436                 optional<string> c = f.optional_string_child ("Container");
437                 if (c) {
438                         _container = Container::from_id (c.get ());
439                 }
440         }
441
442         _scaler = Scaler::from_id (f.string_child ("Scaler"));
443         _ab = f.bool_child ("AB");
444         _with_subtitles = f.bool_child ("WithSubtitles");
445         _subtitle_offset = f.number_child<float> ("SubtitleOffset");
446         _subtitle_scale = f.number_child<float> ("SubtitleScale");
447         _colour_lut = f.number_child<int> ("ColourLUT");
448         _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
449         _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata"));
450         _dcp_video_frame_rate = f.number_child<int> ("DCPVideoFrameRate");
451         _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate"));
452         _dcp_audio_channels = f.number_child<int> ("DCPAudioChannels");
453
454         _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"));
455
456         _dirty = false;
457 }
458
459 /** Given a directory name, return its full path within the Film's directory.
460  *  The directory (and its parents) will be created if they do not exist.
461  */
462 string
463 Film::dir (string d) const
464 {
465         boost::mutex::scoped_lock lm (_directory_mutex);
466         
467         boost::filesystem::path p;
468         p /= _directory;
469         p /= d;
470         
471         boost::filesystem::create_directories (p);
472         
473         return p.string ();
474 }
475
476 /** Given a file or directory name, return its full path within the Film's directory.
477  *  _directory_mutex must not be locked on entry.
478  *  Any required parent directories will be created.
479  */
480 string
481 Film::file (string f) const
482 {
483         boost::mutex::scoped_lock lm (_directory_mutex);
484
485         boost::filesystem::path p;
486         p /= _directory;
487         p /= f;
488
489         boost::filesystem::create_directories (p.parent_path ());
490         
491         return p.string ();
492 }
493
494 /** @return a DCI-compliant name for a DCP of this film */
495 string
496 Film::dci_name (bool if_created_now) const
497 {
498         stringstream d;
499
500         string fixed_name = to_upper_copy (name());
501         for (size_t i = 0; i < fixed_name.length(); ++i) {
502                 if (fixed_name[i] == ' ') {
503                         fixed_name[i] = '-';
504                 }
505         }
506
507         /* Spec is that the name part should be maximum 14 characters, as I understand it */
508         if (fixed_name.length() > 14) {
509                 fixed_name = fixed_name.substr (0, 14);
510         }
511
512         d << fixed_name;
513
514         if (dcp_content_type()) {
515                 d << "_" << dcp_content_type()->dci_name();
516         }
517
518         if (container()) {
519                 d << "_" << container()->dci_name();
520         }
521
522         DCIMetadata const dm = dci_metadata ();
523
524         if (!dm.audio_language.empty ()) {
525                 d << "_" << dm.audio_language;
526                 if (!dm.subtitle_language.empty()) {
527                         d << "-" << dm.subtitle_language;
528                 } else {
529                         d << "-XX";
530                 }
531         }
532
533         if (!dm.territory.empty ()) {
534                 d << "_" << dm.territory;
535                 if (!dm.rating.empty ()) {
536                         d << "-" << dm.rating;
537                 }
538         }
539
540         d << "_51_2K";
541
542         if (!dm.studio.empty ()) {
543                 d << "_" << dm.studio;
544         }
545
546         if (if_created_now) {
547                 d << "_" << boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ());
548         } else {
549                 d << "_" << boost::gregorian::to_iso_string (_dci_date);
550         }
551
552         if (!dm.facility.empty ()) {
553                 d << "_" << dm.facility;
554         }
555
556         if (!dm.package_type.empty ()) {
557                 d << "_" << dm.package_type;
558         }
559
560         return d.str ();
561 }
562
563 /** @return name to give the DCP */
564 string
565 Film::dcp_name (bool if_created_now) const
566 {
567         if (use_dci_name()) {
568                 return dci_name (if_created_now);
569         }
570
571         return name();
572 }
573
574
575 void
576 Film::set_directory (string d)
577 {
578         boost::mutex::scoped_lock lm (_state_mutex);
579         _directory = d;
580         _dirty = true;
581 }
582
583 void
584 Film::set_name (string n)
585 {
586         {
587                 boost::mutex::scoped_lock lm (_state_mutex);
588                 _name = n;
589         }
590         signal_changed (NAME);
591 }
592
593 void
594 Film::set_use_dci_name (bool u)
595 {
596         {
597                 boost::mutex::scoped_lock lm (_state_mutex);
598                 _use_dci_name = u;
599         }
600         signal_changed (USE_DCI_NAME);
601 }
602
603 void
604 Film::set_dcp_content_type (DCPContentType const * t)
605 {
606         {
607                 boost::mutex::scoped_lock lm (_state_mutex);
608                 _dcp_content_type = t;
609         }
610         signal_changed (DCP_CONTENT_TYPE);
611 }
612
613 void
614 Film::set_container (Container const * c)
615 {
616         {
617                 boost::mutex::scoped_lock lm (_state_mutex);
618                 _container = c;
619         }
620         signal_changed (CONTAINER);
621 }
622
623 void
624 Film::set_scaler (Scaler const * s)
625 {
626         {
627                 boost::mutex::scoped_lock lm (_state_mutex);
628                 _scaler = s;
629         }
630         signal_changed (SCALER);
631 }
632
633 void
634 Film::set_ab (bool a)
635 {
636         {
637                 boost::mutex::scoped_lock lm (_state_mutex);
638                 _ab = a;
639         }
640         signal_changed (AB);
641 }
642
643 void
644 Film::set_with_subtitles (bool w)
645 {
646         {
647                 boost::mutex::scoped_lock lm (_state_mutex);
648                 _with_subtitles = w;
649         }
650         signal_changed (WITH_SUBTITLES);
651 }
652
653 void
654 Film::set_subtitle_offset (int o)
655 {
656         {
657                 boost::mutex::scoped_lock lm (_state_mutex);
658                 _subtitle_offset = o;
659         }
660         signal_changed (SUBTITLE_OFFSET);
661 }
662
663 void
664 Film::set_subtitle_scale (float s)
665 {
666         {
667                 boost::mutex::scoped_lock lm (_state_mutex);
668                 _subtitle_scale = s;
669         }
670         signal_changed (SUBTITLE_SCALE);
671 }
672
673 void
674 Film::set_colour_lut (int i)
675 {
676         {
677                 boost::mutex::scoped_lock lm (_state_mutex);
678                 _colour_lut = i;
679         }
680         signal_changed (COLOUR_LUT);
681 }
682
683 void
684 Film::set_j2k_bandwidth (int b)
685 {
686         {
687                 boost::mutex::scoped_lock lm (_state_mutex);
688                 _j2k_bandwidth = b;
689         }
690         signal_changed (J2K_BANDWIDTH);
691 }
692
693 void
694 Film::set_dci_metadata (DCIMetadata m)
695 {
696         {
697                 boost::mutex::scoped_lock lm (_state_mutex);
698                 _dci_metadata = m;
699         }
700         signal_changed (DCI_METADATA);
701 }
702
703
704 void
705 Film::set_dcp_video_frame_rate (int f)
706 {
707         {
708                 boost::mutex::scoped_lock lm (_state_mutex);
709                 _dcp_video_frame_rate = f;
710         }
711         signal_changed (DCP_VIDEO_FRAME_RATE);
712 }
713
714 void
715 Film::signal_changed (Property p)
716 {
717         {
718                 boost::mutex::scoped_lock lm (_state_mutex);
719                 _dirty = true;
720         }
721
722         switch (p) {
723         case Film::CONTENT:
724                 set_dcp_video_frame_rate (_playlist->best_dcp_frame_rate ());
725                 break;
726         default:
727                 break;
728         }
729
730         if (ui_signaller) {
731                 ui_signaller->emit (boost::bind (boost::ref (Changed), p));
732         }
733 }
734
735 void
736 Film::set_dci_date_today ()
737 {
738         _dci_date = boost::gregorian::day_clock::local_day ();
739 }
740
741 string
742 Film::info_path (int f) const
743 {
744         boost::filesystem::path p;
745         p /= info_dir ();
746
747         stringstream s;
748         s.width (8);
749         s << setfill('0') << f << ".md5";
750
751         p /= s.str();
752
753         /* info_dir() will already have added any initial bit of the path,
754            so don't call file() on this.
755         */
756         return p.string ();
757 }
758
759 string
760 Film::j2c_path (int f, bool t) const
761 {
762         boost::filesystem::path p;
763         p /= "j2c";
764         p /= video_state_identifier ();
765
766         stringstream s;
767         s.width (8);
768         s << setfill('0') << f << ".j2c";
769
770         if (t) {
771                 s << ".tmp";
772         }
773
774         p /= s.str();
775         return file (p.string ());
776 }
777
778 /** Make an educated guess as to whether we have a complete DCP
779  *  or not.
780  *  @return true if we do.
781  */
782
783 bool
784 Film::have_dcp () const
785 {
786         try {
787                 libdcp::DCP dcp (dir (dcp_name()));
788                 dcp.read ();
789         } catch (...) {
790                 return false;
791         }
792
793         return true;
794 }
795
796 shared_ptr<Player>
797 Film::player () const
798 {
799         return shared_ptr<Player> (new Player (shared_from_this (), _playlist));
800 }
801
802 shared_ptr<Playlist>
803 Film::playlist () const
804 {
805         boost::mutex::scoped_lock lm (_state_mutex);
806         return _playlist;
807 }
808
809 Playlist::ContentList
810 Film::content () const
811 {
812         return _playlist->content ();
813 }
814
815 void
816 Film::add_content (shared_ptr<Content> c)
817 {
818         _playlist->add (c);
819         examine_content (c);
820 }
821
822 void
823 Film::remove_content (shared_ptr<Content> c)
824 {
825         _playlist->remove (c);
826 }
827
828 Time
829 Film::length () const
830 {
831         return _playlist->length ();
832 }
833
834 bool
835 Film::has_subtitles () const
836 {
837         return _playlist->has_subtitles ();
838 }
839
840 OutputVideoFrame
841 Film::best_dcp_video_frame_rate () const
842 {
843         return _playlist->best_dcp_frame_rate ();
844 }
845
846 void
847 Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
848 {
849         if (p == VideoContentProperty::VIDEO_FRAME_RATE) {
850                 set_dcp_video_frame_rate (_playlist->best_dcp_frame_rate ());
851         } 
852
853         if (ui_signaller) {
854                 ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p));
855         }
856 }
857
858 void
859 Film::playlist_changed ()
860 {
861         signal_changed (CONTENT);
862 }       
863
864 int
865 Film::loop () const
866 {
867         return _playlist->loop ();
868 }
869
870 void
871 Film::set_loop (int c)
872 {
873         _playlist->set_loop (c);
874 }
875
876 OutputAudioFrame
877 Film::time_to_audio_frames (Time t) const
878 {
879         return t * dcp_audio_frame_rate () / TIME_HZ;
880 }
881
882 OutputVideoFrame
883 Film::time_to_video_frames (Time t) const
884 {
885         return t * dcp_video_frame_rate () / TIME_HZ;
886 }
887
888 Time
889 Film::audio_frames_to_time (OutputAudioFrame f) const
890 {
891         return f * TIME_HZ / dcp_audio_frame_rate ();
892 }
893
894 Time
895 Film::video_frames_to_time (OutputVideoFrame f) const
896 {
897         return f * TIME_HZ / dcp_video_frame_rate ();
898 }
899
900 OutputAudioFrame
901 Film::dcp_audio_frame_rate () const
902 {
903         /* XXX */
904         return 48000;
905 }