/*
- Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
- This program is free software; you can redistribute it and/or modify
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
- This program is distributed in the hope that it will be useful,
+ DCP-o-matic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
*/
#include "job.h"
#include "cross.h"
#include "audio_buffers.h"
-#include "md5_digester.h"
-#include "data.h"
#include "version.h"
#include "font.h"
#include "util.h"
#include "reel_writer.h"
#include <dcp/cpl.h>
+#include <dcp/locale_convert.h>
#include <boost/foreach.hpp>
#include <fstream>
#include <cerrno>
#include <iostream>
+#include <cfloat>
#include "i18n.h"
using std::string;
using std::list;
using std::cout;
+using std::map;
+using std::min;
+using std::max;
using boost::shared_ptr;
using boost::weak_ptr;
using boost::dynamic_pointer_cast;
+using dcp::Data;
Writer::Writer (shared_ptr<const Film> film, weak_ptr<Job> j)
: _film (film)
, _repeat_written (0)
, _pushed_to_disk (0)
{
- /* Remove any old DCP */
- boost::filesystem::remove_all (_film->dir (_film->dcp_name ()));
-
shared_ptr<Job> job = _job.lock ();
DCPOMATIC_ASSERT (job);
- BOOST_FOREACH (DCPTimePeriod p, _film->reels ()) {
- _reels.push_back (ReelWriter (film, p, job));
+ int reel_index = 0;
+ list<DCPTimePeriod> const reels = _film->reels ();
+ BOOST_FOREACH (DCPTimePeriod p, reels) {
+ _reels.push_back (ReelWriter (film, p, job, reel_index++, reels.size(), _film->content_summary(p)));
}
/* We can keep track of the current audio and subtitle reels easily because audio
_subtitle_reel = _reels.begin ();
/* Check that the signer is OK if we need one */
- if (_film->is_signed() && !Config::instance()->signer_chain()->valid ()) {
- throw InvalidSignerError ();
+ string reason;
+ if (_film->is_signed() && !Config::instance()->signer_chain()->valid(&reason)) {
+ throw InvalidSignerError (reason);
}
-
- job->sub (_("Encoding image data"));
}
void
_empty_condition.notify_all ();
}
-/** Write one video frame's worth of audio frames to the DCP.
+/** Write some audio frames to the DCP.
* @param audio Audio data or 0 if there is no audio to be written here (i.e. it is referenced).
* This method is not thread safe.
*/
void
Writer::write (shared_ptr<const AudioBuffers> audio)
{
- if (_audio_reel == _reels.end ()) {
- /* This audio is off the end of the last reel; ignore it */
- return;
- }
+ /* The audio we get might span a reel boundary, and if so we have to write it in bits */
+
+ int32_t offset = 0;
+ while (offset < audio->frames ()) {
- _audio_reel->write (audio);
+ if (_audio_reel == _reels.end ()) {
+ /* This audio is off the end of the last reel; ignore it */
+ return;
+ }
- /* written is in video frames, not audio frames */
- if (_audio_reel->total_written_audio_frames() >= _audio_reel->period().duration().frames_floor (_film->video_frame_rate())) {
- ++_audio_reel;
+ int32_t const remaining = audio->frames() - offset;
+ int32_t const reel_space = _audio_reel->period().duration().frames_floor(_film->audio_frame_rate()) - _audio_reel->total_written_audio_frames();
+
+ if (remaining <= reel_space) {
+ /* Easy case: we can write all the audio to this reel */
+ _audio_reel->write (audio);
+ offset += remaining;
+ } else {
+ /* Write the part we can */
+ shared_ptr<AudioBuffers> part (new AudioBuffers (audio->channels(), reel_space));
+ part->copy_from (audio.get(), reel_space, offset, 0);
+ _audio_reel->write (part);
+ ++_audio_reel;
+ offset += reel_space;
+ }
}
}
LOG_WARNING (N_("Finishing writer with a left-over queue of %1:"), _queue.size());
for (list<QueueItem>::const_iterator i = _queue.begin(); i != _queue.end(); ++i) {
if (i->type == QueueItem::FULL) {
- LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i->frame, i->eyes);
+ LOG_WARNING (N_("- type FULL, frame %1, eyes %2"), i->frame, (int) i->eyes);
} else {
- LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i->size, i->frame, i->eyes);
+ LOG_WARNING (N_("- type FAKE, size %1, frame %2, eyes %3"), i->size, i->frame, (int) i->eyes);
}
}
}
switch (qi.type) {
case QueueItem::FULL:
- LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, qi.eyes);
+ LOG_DEBUG_ENCODE (N_("Writer FULL-writes %1 (%2)"), qi.frame, (int) qi.eyes);
if (!qi.encoded) {
qi.encoded = Data (_film->j2c_path (qi.reel, qi.frame, qi.eyes, false));
}
}
lock.lock ();
-
- shared_ptr<Job> job = _job.lock ();
- DCPOMATIC_ASSERT (job);
- int64_t total = _film->length().frames_round (_film->video_frame_rate ());
- if (_film->three_d ()) {
- /* _full_written and so on are incremented for each eye, so we need to double the total
- frames to get the correct progress.
- */
- total *= 2;
- }
- if (total) {
- job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
- }
}
while (_queued_full_in_memory > _maximum_frames_in_memory) {
DCPOMATIC_ASSERT (i != _queue.rend());
++_pushed_to_disk;
+ /* For the log message below */
+ int const awaiting = _reels[_queue.front().reel].last_written_video_frame();
lock.unlock ();
/* i is valid here, even though we don't hold a lock on the mutex,
thread could erase the last item in the list.
*/
- LOG_GENERAL ("Writer full; pushes %1 to disk", i->frame);
+ LOG_GENERAL ("Writer full; pushes %1 to disk while awaiting %2", i->frame, awaiting);
i->encoded->write_via_temp (
_film->j2c_path (i->reel, i->frame, i->eyes, true),
return;
}
+ LOG_GENERAL_NC ("Terminating writer thread");
+
terminate_thread (true);
+ LOG_GENERAL_NC ("Finishing ReelWriters");
+
BOOST_FOREACH (ReelWriter& i, _reels) {
i.finish ();
}
+ LOG_GENERAL_NC ("Writing XML");
+
dcp::DCP dcp (_film->dir (_film->dcp_name()));
shared_ptr<dcp::CPL> cpl (
dcp.add (cpl);
+ /* Calculate digests for each reel in parallel */
+
+ shared_ptr<Job> job = _job.lock ();
+ job->sub (_("Computing digests"));
+
+ boost::asio::io_service service;
+ boost::thread_group pool;
+
+ shared_ptr<boost::asio::io_service::work> work (new boost::asio::io_service::work (service));
+
+ int const threads = max (1, Config::instance()->master_encoding_threads ());
+
+ for (int i = 0; i < threads; ++i) {
+ pool.create_thread (boost::bind (&boost::asio::io_service::run, &service));
+ }
+
BOOST_FOREACH (ReelWriter& i, _reels) {
+ boost::function<void (float)> set_progress = boost::bind (&Writer::set_digest_progress, this, job.get(), _1);
+ service.post (boost::bind (&ReelWriter::calculate_digests, &i, set_progress));
+ }
- cpl->add (i.create_reel (_reel_assets, _fonts));
+ work.reset ();
+ pool.join_all ();
+ service.stop ();
- shared_ptr<Job> job = _job.lock ();
- DCPOMATIC_ASSERT (job);
- i.calculate_digests (job);
+ /* Add reels to CPL */
+
+ BOOST_FOREACH (ReelWriter& i, _reels) {
+ cpl->add (i.create_reel (_reel_assets, _fonts));
}
dcp::XMLMetadata meta;
+ meta.annotation_text = cpl->annotation_text ();
meta.creator = Config::instance()->dcp_creator ();
if (meta.creator.empty ()) {
meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit);
if (_film->is_signed ()) {
signer = Config::instance()->signer_chain ();
/* We did check earlier, but check again here to be on the safe side */
- if (!signer->valid ()) {
- throw InvalidSignerError ();
+ string reason;
+ if (!signer->valid (&reason)) {
+ throw InvalidSignerError (reason);
}
}
- dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer);
+ dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer, Config::instance()->dcp_metadata_filename_format());
LOG_GENERAL (
N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT, %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
);
+
+ write_cover_sheet ();
+}
+
+void
+Writer::write_cover_sheet ()
+{
+ boost::filesystem::path const cover = _film->file ("COVER_SHEET.txt");
+ FILE* f = fopen_boost (cover, "w");
+ if (!f) {
+ throw OpenFileError (cover, errno, false);
+ }
+
+ string text = Config::instance()->cover_sheet ();
+ boost::algorithm::replace_all (text, "$CPL_NAME", _film->name());
+ boost::algorithm::replace_all (text, "$TYPE", _film->dcp_content_type()->pretty_name());
+ boost::algorithm::replace_all (text, "$CONTAINER", _film->container()->container_nickname());
+ boost::algorithm::replace_all (text, "$AUDIO_LANGUAGE", _film->isdcf_metadata().audio_language);
+ boost::algorithm::replace_all (text, "$SUBTITLE_LANGUAGE", _film->isdcf_metadata().subtitle_language);
+
+ boost::uintmax_t size = 0;
+ for (
+ boost::filesystem::recursive_directory_iterator i = boost::filesystem::recursive_directory_iterator(_film->dir(_film->dcp_name()));
+ i != boost::filesystem::recursive_directory_iterator();
+ ++i) {
+ if (boost::filesystem::is_regular_file (i->path ())) {
+ size += boost::filesystem::file_size (i->path ());
+ }
+ }
+
+ if (size > (1000000000L)) {
+ boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1GB", dcp::locale_convert<string> (size / 1000000000.0, 1, true)));
+ } else {
+ boost::algorithm::replace_all (text, "$SIZE", String::compose ("%1MB", dcp::locale_convert<string> (size / 1000000.0, 1, true)));
+ }
+
+ pair<int, int> ch = audio_channel_types (_film->mapped_audio_channels(), _film->audio_channels());
+ string description = String::compose("%1.%2", ch.first, ch.second);
+
+ if (description == "0.0") {
+ description = _("None");
+ } else if (description == "1.0") {
+ description = _("Mono");
+ } else if (description == "2.0") {
+ description = _("Stereo");
+ }
+ boost::algorithm::replace_all (text, "$AUDIO", description);
+
+ int h, m, s, fr;
+ _film->length().split (_film->video_frame_rate(), h, m, s, fr);
+ string length;
+ if (h == 0 && m == 0) {
+ length = String::compose("%1s", s);
+ } else if (h == 0 && m > 0) {
+ length = String::compose("%1m%2s", m, s);
+ } else if (h > 0 && m > 0) {
+ length = String::compose("%1h%2m%3s", h, m, s);
+ }
+
+ boost::algorithm::replace_all (text, "$LENGTH", length);
+
+ fwrite (text.c_str(), 1, text.length(), f);
+ fclose (f);
}
/** @param frame Frame index within the whole DCP.
}
void
-Writer::write (PlayerSubtitles subs)
+Writer::write (PlayerSubtitles subs, DCPTimePeriod period)
{
if (subs.text.empty ()) {
return;
}
- if (_subtitle_reel->period().to <= subs.from) {
+ if (_subtitle_reel->period().to <= period.from) {
++_subtitle_reel;
}
void
Writer::write (list<shared_ptr<Font> > fonts)
{
- /* Just keep a list of fonts and we'll deal with them in ::finish */
- copy (fonts.begin (), fonts.end (), back_inserter (_fonts));
+ /* Just keep a list of unique fonts and we'll deal with them in ::finish */
+
+ BOOST_FOREACH (shared_ptr<Font> i, fonts) {
+ bool got = false;
+ BOOST_FOREACH (shared_ptr<Font> j, _fonts) {
+ if (*i == *j) {
+ got = true;
+ }
+ }
+
+ if (!got) {
+ _fonts.push_back (i);
+ }
+ }
}
bool
void
Writer::set_encoder_threads (int threads)
{
- _maximum_frames_in_memory = lrint (threads * 1.1);
+ _maximum_frames_in_memory = lrint (threads * Config::instance()->frames_in_memory_multiplier());
}
void
DCPOMATIC_ASSERT (i < _reels.size ());
return i;
}
+
+void
+Writer::set_digest_progress (Job* job, float progress)
+{
+ /* I believe this is thread-safe */
+ _digest_progresses[boost::this_thread::get_id()] = progress;
+
+ boost::mutex::scoped_lock lm (_digest_progresses_mutex);
+ float min_progress = FLT_MAX;
+ for (map<boost::thread::id, float>::const_iterator i = _digest_progresses.begin(); i != _digest_progresses.end(); ++i) {
+ min_progress = min (min_progress, i->second);
+ }
+
+ job->set_progress (min_progress);
+}