Content changes should mark the film dirty (#1810).
[dcpomatic.git] / src / lib / film.cc
index efc89e0eb6be3b27c6a7c04a31b82ee32c68f54b..867944689a029189e1b481a4ca57e7b5406fc0a8 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2019 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -23,6 +23,7 @@
  *  how they should be presented in a DCP.
  */
 
+#include "atmos_content.h"
 #include "film.h"
 #include "job.h"
 #include "util.h"
 #include "text_content.h"
 #include "ffmpeg_content.h"
 #include "dcp_content.h"
-#include "screen_kdm.h"
+#include "kdm_with_metadata.h"
 #include "cinema.h"
 #include "change_signaller.h"
 #include "check_content_change_job.h"
+#include "ffmpeg_subtitle_stream.h"
+#include "font.h"
 #include <libcxml/cxml.h>
 #include <dcp/cpl.h>
 #include <dcp/certificate_chain.h>
@@ -93,6 +96,7 @@ using std::copy;
 using std::back_inserter;
 using std::map;
 using std::exception;
+using std::find;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
@@ -129,8 +133,10 @@ string const Film::metadata_file = "metadata.xml";
  * 36 -> 37
  * TextContent can be in a Caption tag, and some of the tag names
  * have had Subtitle prefixes or suffixes removed.
+ * 37 -> 38
+ * VideoContent scale expressed just as "guess" or "custom"
  */
-int const Film::current_state_version = 37;
+int const Film::current_state_version = 38;
 
 /** Construct a Film object in a given directory.
  *
@@ -143,7 +149,6 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _dcp_content_type (Config::instance()->default_dcp_content_type ())
        , _container (Config::instance()->default_container ())
        , _resolution (RESOLUTION_2K)
-       , _signed (true)
        , _encrypted (false)
        , _context_id (dcp::make_uuid ())
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
@@ -156,17 +161,20 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _audio_processor (0)
        , _reel_type (REELTYPE_SINGLE)
        , _reel_length (2000000000)
-       , _upload_after_make_dcp (Config::instance()->default_upload_after_make_dcp())
        , _reencode_j2k (false)
        , _user_explicit_video_frame_rate (false)
+       , _user_explicit_container (false)
+       , _user_explicit_resolution (false)
        , _state_version (current_state_version)
        , _dirty (false)
+       , _tolerant (false)
 {
        set_isdcf_date_today ();
 
        _playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1));
-       _playlist_order_changed_connection = _playlist->OrderChanged.connect (bind (&Film::playlist_order_changed, this));
+       _playlist_order_changed_connection = _playlist->OrderChange.connect (bind (&Film::playlist_order_changed, this));
        _playlist_content_change_connection = _playlist->ContentChange.connect (bind (&Film::playlist_content_change, this, _1, _2, _3, _4));
+       _playlist_length_change_connection = _playlist->LengthChange.connect (bind(&Film::playlist_length_change, this));
 
        if (dir) {
                /* Make state.directory a complete path without ..s (where possible)
@@ -178,7 +186,8 @@ Film::Film (optional<boost::filesystem::path> dir)
                boost::filesystem::path result;
                for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
                        if (*i == "..") {
-                               if (boost::filesystem::is_symlink (result) || result.filename() == "..") {
+                               boost::system::error_code ec;
+                               if (boost::filesystem::is_symlink(result, ec) || result.filename() == "..") {
                                        result /= *i;
                                } else {
                                        result = result.parent_path ();
@@ -223,7 +232,8 @@ Film::video_identifier () const
                + "_" + raw_convert<string>(j2k_bandwidth());
 
        if (encrypted ()) {
-               s += "_E";
+               /* This is insecure but hey, the key is in plaintext in metadata.xml */
+               s += "_E" + _key.hex();
        } else {
                s += "_P";
        }
@@ -297,9 +307,45 @@ Film::audio_analysis_path (shared_ptr<const Playlist> playlist) const
        return p;
 }
 
-/** Add suitable Jobs to the JobManager to create a DCP for this Film */
+
+boost::filesystem::path
+Film::subtitle_analysis_path (shared_ptr<const Content> content) const
+{
+       boost::filesystem::path p = dir ("analysis");
+
+       Digester digester;
+       digester.add (content->digest());
+
+       if (!content->text.empty()) {
+               shared_ptr<TextContent> tc = content->text.front();
+               digester.add (tc->x_scale());
+               digester.add (tc->y_scale());
+               BOOST_FOREACH (shared_ptr<dcpomatic::Font> i, tc->fonts()) {
+                       digester.add (i->id());
+               }
+               if (tc->effect()) {
+                       digester.add (tc->effect().get());
+               }
+               digester.add (tc->line_spacing());
+               digester.add (tc->outline_width());
+       }
+
+       shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent>(content);
+       if (fc) {
+               digester.add (fc->subtitle_stream()->identifier());
+       }
+
+       p /= digester.get ();
+       return p;
+}
+
+
+/** Add suitable Jobs to the JobManager to create a DCP for this Film.
+ *  @param gui true if this is being called from a GUI tool.
+ *  @param check true to check the content in the project for changes before making the DCP.
+ */
 void
-Film::make_dcp ()
+Film::make_dcp (bool gui, bool check)
 {
        if (dcp_name().find ("/") != string::npos) {
                throw BadSettingError (_("name"), _("Cannot contain slashes"));
@@ -313,6 +359,10 @@ Film::make_dcp ()
                throw runtime_error (_("You must add some content to the DCP before creating it"));
        }
 
+       if (length() == DCPTime()) {
+               throw runtime_error (_("The DCP is empty, perhaps because all the content has zero length."));
+       }
+
        if (dcp_content_type() == 0) {
                throw MissingSettingError (_("content type"));
        }
@@ -353,8 +403,12 @@ Film::make_dcp ()
 
        shared_ptr<TranscodeJob> tj (new TranscodeJob (shared_from_this()));
        tj->set_encoder (shared_ptr<Encoder> (new DCPEncoder (shared_from_this(), tj)));
-       shared_ptr<CheckContentChangeJob> cc (new CheckContentChangeJob (shared_from_this(), tj));
-       JobManager::instance()->add (cc);
+       if (check) {
+               shared_ptr<CheckContentChangeJob> cc (new CheckContentChangeJob(shared_from_this(), tj, gui));
+               JobManager::instance()->add (cc);
+       } else {
+               JobManager::instance()->add (tj);
+       }
 }
 
 /** Start a job to send our DCP to the configured TMS */
@@ -392,7 +446,6 @@ Film::metadata (bool with_content_paths) const
        root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0");
        root->add_child("Sequence")->add_child_text (_sequence ? "1" : "0");
        root->add_child("Interop")->add_child_text (_interop ? "1" : "0");
-       root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
        root->add_child("ContextID")->add_child_text (_context_id);
@@ -401,7 +454,6 @@ Film::metadata (bool with_content_paths) const
        }
        root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type)));
        root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
-       root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
        root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0");
        root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
        for (map<dcp::Marker, DCPTime>::const_iterator i = _markers.begin(); i != _markers.end(); ++i) {
@@ -412,6 +464,9 @@ Film::metadata (bool with_content_paths) const
        BOOST_FOREACH (dcp::Rating i, _ratings) {
                i.as_xml (root->add_child("Rating"));
        }
+       root->add_child("ContentVersion")->add_child_text(_content_version);
+       root->add_child("UserExplicitContainer")->add_child_text(_user_explicit_container ? "1" : "0");
+       root->add_child("UserExplicitResolution")->add_child_text(_user_explicit_resolution ? "1" : "0");
        _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
 
        return doc;
@@ -458,6 +513,10 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                path = file (metadata_file);
        }
 
+       if (!boost::filesystem::exists(*path)) {
+               throw FileNotFoundError(*path);
+       }
+
        cxml::Document f ("Metadata");
        f.read_file (path.get ());
 
@@ -505,7 +564,6 @@ Film::read_metadata (optional<boost::filesystem::path> path)
        _resolution = string_to_resolution (f.string_child ("Resolution"));
        _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
        _video_frame_rate = f.number_child<int> ("VideoFrameRate");
-       _signed = f.optional_bool_child("Signed").get_value_or (true);
        _encrypted = f.bool_child ("Encrypted");
        _audio_channels = f.number_child<int> ("AudioChannels");
        /* We used to allow odd numbers (and zero) channels, but it's just not worth
@@ -534,9 +592,15 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                _audio_processor = 0;
        }
 
+       if (_audio_processor && !Config::instance()->show_experimental_audio_processors()) {
+               list<AudioProcessor const *> ap = AudioProcessor::visible();
+               if (find(ap.begin(), ap.end(), _audio_processor) == ap.end()) {
+                       Config::instance()->set_show_experimental_audio_processors(true);
+               }
+       }
+
        _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(REELTYPE_SINGLE)));
        _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000);
-       _upload_after_make_dcp = f.optional_bool_child("UploadAfterMakeDCP").get_value_or (false);
        _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false);
        _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
 
@@ -548,6 +612,12 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                _ratings.push_back (dcp::Rating(i));
        }
 
+       _content_version = f.optional_string_child("ContentVersion").get_value_or("");
+
+       /* Disable guessing for files made in previous DCP-o-matic versions */
+       _user_explicit_container = f.optional_bool_child("UserExplicitContainer").get_value_or(true);
+       _user_explicit_resolution = f.optional_bool_child("UserExplicitResolution").get_value_or(true);
+
        list<string> notes;
        _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes);
 
@@ -722,11 +792,7 @@ Film::isdcf_name (bool if_created_now) const
                BOOST_FOREACH (shared_ptr<Content> i, content ()) {
                        if (i->video) {
                                /* Here's the first piece of video content */
-                               if (i->video->scale().ratio ()) {
-                                       content_ratio = i->video->scale().ratio ();
-                               } else {
-                                       content_ratio = Ratio::from_ratio (i->video->size().ratio ());
-                               }
+                               content_ratio = Ratio::nearest_from_ratio(i->video->scaled_size(frame_size()).ratio());
                                break;
                        }
                }
@@ -797,14 +863,21 @@ Film::isdcf_name (bool if_created_now) const
 
        /* Count mapped audio channels */
 
-       pair<int, int> ch = audio_channel_types (mapped_audio_channels(), audio_channels());
+       list<int> mapped = mapped_audio_channels ();
+
+       pair<int, int> ch = audio_channel_types (mapped, audio_channels());
        if (!ch.first && !ch.second) {
                d += "_MOS";
        } else if (ch.first) {
                d += String::compose("_%1%2", ch.first, ch.second);
        }
 
-       /* XXX: HI/VI */
+       if (audio_channels() > static_cast<int>(dcp::HI) && find(mapped.begin(), mapped.end(), dcp::HI) != mapped.end()) {
+               d += "-HI";
+       }
+       if (audio_channels() > static_cast<int>(dcp::VI) && find(mapped.begin(), mapped.end(), dcp::VI) != mapped.end()) {
+               d += "-VI";
+       }
 
        d += "_" + resolution_to_string (_resolution);
 
@@ -899,20 +972,39 @@ Film::set_dcp_content_type (DCPContentType const * t)
        _dcp_content_type = t;
 }
 
+
+/** @param explicit_user true if this is being set because of
+ *  a direct user request, false if it is being done because
+ *  DCP-o-matic is guessing the best container to use.
+ */
 void
-Film::set_container (Ratio const * c)
+Film::set_container (Ratio const * c, bool explicit_user)
 {
        ChangeSignaller<Film> ch (this, CONTAINER);
        _container = c;
+
+       if (explicit_user) {
+               _user_explicit_container = true;
+       }
 }
 
+
+/** @param explicit_user true if this is being set because of
+ *  a direct user request, false if it is being done because
+ *  DCP-o-matic is guessing the best resolution to use.
+ */
 void
-Film::set_resolution (Resolution r)
+Film::set_resolution (Resolution r, bool explicit_user)
 {
        ChangeSignaller<Film> ch (this, RESOLUTION);
        _resolution = r;
+
+       if (explicit_user) {
+               _user_explicit_resolution = true;
+       }
 }
 
+
 void
 Film::set_j2k_bandwidth (int b)
 {
@@ -990,13 +1082,6 @@ Film::set_reel_length (int64_t r)
        _reel_length = r;
 }
 
-void
-Film::set_upload_after_make_dcp (bool u)
-{
-       ChangeSignaller<Film> ch (this, UPLOAD_AFTER_MAKE_DCP);
-       _upload_after_make_dcp = u;
-}
-
 void
 Film::set_reencode_j2k (bool r)
 {
@@ -1107,13 +1192,6 @@ Film::cpls () const
        return out;
 }
 
-void
-Film::set_signed (bool s)
-{
-       ChangeSignaller<Film> ch (this, SIGNED);
-       _signed = s;
-}
-
 void
 Film::set_encrypted (bool e)
 {
@@ -1121,13 +1199,6 @@ Film::set_encrypted (bool e)
        _encrypted = e;
 }
 
-void
-Film::set_key (dcp::Key key)
-{
-       ChangeSignaller<Film> ch (this, KEY);
-       _key = key;
-}
-
 ContentList
 Film::content () const
 {
@@ -1197,12 +1268,58 @@ Film::add_content (shared_ptr<Content> c)
        }
 
        _playlist->add (shared_from_this(), c);
+
+       maybe_set_container_and_resolution ();
+       if (c->atmos) {
+               set_audio_channels (14);
+               set_interop (false);
+       }
+}
+
+
+void
+Film::maybe_set_container_and_resolution ()
+{
+       /* Get the only piece of video content, if there is only one */
+       shared_ptr<VideoContent> video;
+       BOOST_FOREACH (shared_ptr<const Content> i, _playlist->content()) {
+               if (i->video) {
+                       if (!video) {
+                               video = i->video;
+                       } else {
+                               video.reset ();
+                       }
+               }
+       }
+
+       if (video) {
+               /* This is the only piece of video content in this Film.  Use it to make a guess for
+                * DCP container size and resolution, unless the user has already explicitly set these
+                * things.
+                */
+               if (!_user_explicit_container) {
+                       if (video->size().ratio() > 2.3) {
+                               set_container (Ratio::from_id("239"), false);
+                       } else {
+                               set_container (Ratio::from_id("185"), false);
+                       }
+               }
+
+               if (!_user_explicit_resolution) {
+                       if (video->size_after_crop().width > 2048 || video->size_after_crop().height > 1080) {
+                               set_resolution (RESOLUTION_4K, false);
+                       } else {
+                               set_resolution (RESOLUTION_2K, false);
+                       }
+               }
+       }
 }
 
 void
 Film::remove_content (shared_ptr<Content> c)
 {
        _playlist->remove (c);
+       maybe_set_container_and_resolution ();
 }
 
 void
@@ -1217,11 +1334,13 @@ Film::move_content_later (shared_ptr<Content> c)
        _playlist->move_later (shared_from_this(), c);
 }
 
-/** @return length of the film from time 0 to the last thing on the playlist */
+/** @return length of the film from time 0 to the last thing on the playlist,
+ *  with a minimum length of 1 second.
+ */
 DCPTime
 Film::length () const
 {
-       return _playlist->length(shared_from_this()).ceil(video_frame_rate());
+       return max(DCPTime::from_seconds(1), _playlist->length(shared_from_this()).ceil(video_frame_rate()));
 }
 
 int
@@ -1252,9 +1371,20 @@ Film::playlist_content_change (ChangeType type, weak_ptr<Content> c, int p, bool
 
        if (type == CHANGE_TYPE_DONE) {
                emit (boost::bind (boost::ref (ContentChange), type, c, p, frequent));
+               if (!frequent) {
+                       check_settings_consistency ();
+               }
        } else {
                ContentChange (type, c, p, frequent);
        }
+
+       _dirty = true;
+}
+
+void
+Film::playlist_length_change ()
+{
+       LengthChange ();
 }
 
 void
@@ -1264,29 +1394,54 @@ Film::playlist_change (ChangeType type)
        signal_change (type, NAME);
 
        if (type == CHANGE_TYPE_DONE) {
-               /* Check that this change hasn't made our settings inconsistent */
-               bool change_made = false;
-               BOOST_FOREACH (shared_ptr<Content> i, content()) {
-                       shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
-                       if (!d) {
-                               continue;
-                       }
+               check_settings_consistency ();
+       }
 
-                       string why_not;
-                       if (d->reference_video() && !d->can_reference_video(shared_from_this(), why_not)) {
-                               d->set_reference_video(false);
-                               change_made = true;
-                       }
-                       if (d->reference_audio() && !d->can_reference_audio(shared_from_this(), why_not)) {
-                               d->set_reference_audio(false);
-                               change_made = true;
+       _dirty = true;
+}
+
+/** Check for (and if necessary fix) impossible settings combinations, like
+ *  video set to being referenced when it can't be.
+ */
+void
+Film::check_settings_consistency ()
+{
+       optional<int> atmos_rate;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+
+               if (i->atmos) {
+                       int rate = lrintf (i->atmos->edit_rate().as_float());
+                       if (atmos_rate && *atmos_rate != rate) {
+                               Message (_("You have more than one piece of Atmos content, and they do not have the same frame rate.  You must remove some Atmos content."));
+                       } else if (!atmos_rate && rate != video_frame_rate()) {
+                               atmos_rate = rate;
+                               set_video_frame_rate (rate, false);
+                               Message (_("DCP-o-matic had to change your settings so that the film's frame rate is the same as that of your Atmos content."));
                        }
                }
+       }
+
+       bool change_made = false;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (!d) {
+                       continue;
+               }
 
-               if (change_made) {
-                       Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV.  Please review those settings to make sure they are what you want."));
+               string why_not;
+               if (d->reference_video() && !d->can_reference_video(shared_from_this(), why_not)) {
+                       d->set_reference_video(false);
+                       change_made = true;
+               }
+               if (d->reference_audio() && !d->can_reference_audio(shared_from_this(), why_not)) {
+                       d->set_reference_audio(false);
+                       change_made = true;
                }
        }
+
+       if (change_made) {
+               Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV.  Please review those settings to make sure they are what you want."));
+       }
 }
 
 void
@@ -1411,47 +1566,6 @@ Film::make_kdm (
                ).encrypt (signer, recipient, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
 }
 
-/** @param screens Screens to make KDMs for.
- *  @param cpl_file Path to CPL to make KDMs for.
- *  @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
- *  @param until KDM to time expressed as a local time in the time zone of the Screen's Cinema.
- *  @param formulation KDM formulation to use.
- *  @param disable_forensic_marking_picture true to disable forensic marking of picture.
- *  @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio.  If set to 0,
- *  disable all forensic marking; if set above 0, disable forensic marking above that channel.
- */
-list<ScreenKDM>
-Film::make_kdms (
-       list<shared_ptr<Screen> > screens,
-       boost::filesystem::path cpl_file,
-       boost::posix_time::ptime from,
-       boost::posix_time::ptime until,
-       dcp::Formulation formulation,
-       bool disable_forensic_marking_picture,
-       optional<int> disable_forensic_marking_audio
-       ) const
-{
-       list<ScreenKDM> kdms;
-
-       BOOST_FOREACH (shared_ptr<Screen> i, screens) {
-               if (i->recipient) {
-                       dcp::EncryptedKDM const kdm = make_kdm (
-                               i->recipient.get(),
-                               i->trusted_device_thumbprints(),
-                               cpl_file,
-                               dcp::LocalTime (from,  i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0),
-                               dcp::LocalTime (until, i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0),
-                               formulation,
-                               disable_forensic_marking_picture,
-                               disable_forensic_marking_audio
-                               );
-
-                       kdms.push_back (ScreenKDM (i, kdm));
-               }
-       }
-
-       return kdms;
-}
 
 /** @return The approximate disk space required to encode a DCP of this film with the
  *  current settings, in bytes.
@@ -1571,37 +1685,43 @@ Film::reels () const
                break;
        case REELTYPE_BY_VIDEO_CONTENT:
        {
-               optional<DCPTime> last_split;
-               shared_ptr<Content> last_video;
-               BOOST_FOREACH (shared_ptr<Content> c, content ()) {
+               /* Collect all reel boundaries */
+               list<DCPTime> split_points;
+               split_points.push_back (DCPTime());
+               split_points.push_back (len);
+               BOOST_FOREACH (shared_ptr<Content> c, content()) {
                        if (c->video) {
                                BOOST_FOREACH (DCPTime t, c->reel_split_points(shared_from_this())) {
-                                       if (last_split) {
-                                               p.push_back (DCPTimePeriod (last_split.get(), t));
-                                       }
-                                       last_split = t;
+                                       split_points.push_back (t);
                                }
-                               last_video = c;
+                               split_points.push_back (c->end(shared_from_this()));
                        }
                }
 
-               DCPTime video_end = last_video ? last_video->end(shared_from_this()) : DCPTime(0);
-               if (last_split) {
-                       /* Definitely go from the last split to the end of the video content */
-                       p.push_back (DCPTimePeriod (last_split.get(), video_end));
-               }
-
-               if (video_end < len) {
-                       /* And maybe go after that as well if there is any non-video hanging over the end */
-                       p.push_back (DCPTimePeriod (video_end, len));
+               split_points.sort ();
+               split_points.unique ();
+
+               /* Make them into periods, coalescing any that are less than 1 second long */
+               optional<DCPTime> last;
+               BOOST_FOREACH (DCPTime t, split_points) {
+                       if (last && (t - *last) >= DCPTime::from_seconds(1)) {
+                               /* Period from *last to t is long enough; use it and start a new one */
+                               p.push_back (DCPTimePeriod(*last, t));
+                               last = t;
+                       } else if (!last) {
+                               /* That was the first time, so start a new period */
+                               last = t;
+                       }
                }
                break;
        }
        case REELTYPE_BY_LENGTH:
        {
                DCPTime current;
-               /* Integer-divide reel length by the size of one frame to give the number of frames per reel */
-               Frame const reel_in_frames = _reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8);
+               /* Integer-divide reel length by the size of one frame to give the number of frames per reel,
+                * making sure we don't go less than 1s long.
+                */
+               Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate()));
                while (current < len) {
                        DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ()));
                        p.push_back (DCPTimePeriod (current, end));
@@ -1634,7 +1754,6 @@ Film::use_template (string name)
        _resolution = _template_film->_resolution;
        _j2k_bandwidth = _template_film->_j2k_bandwidth;
        _video_frame_rate = _template_film->_video_frame_rate;
-       _signed = _template_film->_signed;
        _encrypted = _template_film->_encrypted;
        _audio_channels = _template_film->_audio_channels;
        _sequence = _template_film->_sequence;
@@ -1643,7 +1762,6 @@ Film::use_template (string name)
        _audio_processor = _template_film->_audio_processor;
        _reel_type = _template_film->_reel_type;
        _reel_length = _template_film->_reel_length;
-       _upload_after_make_dcp = _template_film->_upload_after_make_dcp;
        _isdcf_metadata = _template_film->_isdcf_metadata;
 }
 
@@ -1685,6 +1803,20 @@ Film::references_dcp_audio () const
        return false;
 }
 
+
+bool
+Film::contains_atmos_content () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               if (i->atmos) {
+                       return true;
+               }
+       }
+
+       return false;
+}
+
+
 list<DCPTextTrack>
 Film::closed_caption_tracks () const
 {
@@ -1716,6 +1848,15 @@ Film::unset_marker (dcp::Marker type)
        _markers.erase (type);
 }
 
+
+void
+Film::clear_markers ()
+{
+       ChangeSignaller<Film> ch (this, MARKERS);
+       _markers.clear ();
+}
+
+
 void
 Film::set_ratings (vector<dcp::Rating> r)
 {
@@ -1723,6 +1864,13 @@ Film::set_ratings (vector<dcp::Rating> r)
        _ratings = r;
 }
 
+void
+Film::set_content_version (string v)
+{
+       ChangeSignaller<Film> ch (this, CONTENT_VERSION);
+       _content_version = v;
+}
+
 optional<DCPTime>
 Film::marker (dcp::Marker type) const
 {
@@ -1732,3 +1880,37 @@ Film::marker (dcp::Marker type) const
        }
        return i->second;
 }
+
+shared_ptr<InfoFileHandle>
+Film::info_file_handle (DCPTimePeriod period, bool read) const
+{
+       return shared_ptr<InfoFileHandle> (new InfoFileHandle(_info_file_mutex, info_file(period), read));
+}
+
+InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path file, bool read)
+       : _lock (mutex)
+       , _file (file)
+{
+       if (read) {
+               _handle = fopen_boost (file, "rb");
+               if (!_handle) {
+                       throw OpenFileError (file, errno, OpenFileError::READ);
+               }
+       } else {
+               bool const exists = boost::filesystem::exists (file);
+               if (exists) {
+                       _handle = fopen_boost (file, "r+b");
+               } else {
+                       _handle = fopen_boost (file, "wb");
+               }
+
+               if (!_handle) {
+                       throw OpenFileError (file, errno, exists ? OpenFileError::READ_WRITE : OpenFileError::WRITE);
+               }
+       }
+}
+
+InfoFileHandle::~InfoFileHandle ()
+{
+       fclose (_handle);
+}