+void
+Hints::check_out_of_range_markers ()
+{
+ auto const length = film()->length();
+ for (auto const& i: film()->markers()) {
+ if (i.second >= length) {
+ hint (_("At least one marker comes after the end of the project and will be ignored."));
+ }
+ }
+}
+
+
+void
+Hints::scan_content(shared_ptr<const Film> film)
+{
+ auto const check_loudness_done = check_loudness();
+
+ auto content = film->playlist()->content();
+ auto iter = std::find_if(content.begin(), content.end(), [](shared_ptr<const Content> content) {
+ auto text_iter = std::find_if(content->text.begin(), content->text.end(), [](shared_ptr<const TextContent> text) {
+ return text->use();
+ });
+ return text_iter != content->text.end();
+ });
+
+ auto const have_text = iter != content.end();
+
+ if (check_loudness_done && !have_text) {
+ /* We don't need to check loudness, and we don't have any active text to check,
+ * so a scan of the content is pointless.
+ */
+ return;
+ }
+
+ if (check_loudness_done && have_text) {
+ emit (bind(boost::ref(Progress), _("Examining subtitles and closed captions")));
+ } else if (!check_loudness_done && !have_text) {
+ emit (bind(boost::ref(Progress), _("Examining audio")));
+ } else {
+ emit (bind(boost::ref(Progress), _("Examining audio, subtitles and closed captions")));
+ }
+
+ auto player = make_shared<Player>(film, Image::Alignment::COMPACT);
+ player->set_ignore_video();
+ if (check_loudness_done || _disable_audio_analysis) {
+ /* We don't need to analyse audio because we already loaded a suitable analysis */
+ player->set_ignore_audio();
+ } else {
+ /* Send auto to the analyser to check loudness */
+ player->Audio.connect(bind(&Hints::audio, this, _1, _2));
+ }
+ player->Text.connect(bind(&Hints::text, this, _1, _2, _3, _4));
+
+ struct timeval last_pulse;
+ gettimeofday(&last_pulse, 0);
+
+ _writer->write(player->get_subtitle_fonts());
+
+ while (!player->pass()) {
+
+ struct timeval now;
+ gettimeofday(&now, 0);
+ if ((seconds(now) - seconds(last_pulse)) > 1) {
+ if (_stop) {
+ return;
+ }
+ emit(bind(boost::ref(Pulse)));
+ last_pulse = now;
+ }
+ }
+
+ if (!check_loudness_done) {
+ _analyser.finish();
+ _analyser.get().write(film->audio_analysis_path(film->playlist()));
+ check_loudness();
+ }
+}
+
+
+void
+Hints::thread ()
+try
+{
+ start_of_thread ("Hints");
+
+ auto film = _film.lock ();
+ if (!film) {
+ return;
+ }
+
+ auto content = film->content ();
+
+ check_certificates ();
+ check_interop ();
+ check_big_font_files ();
+ check_few_audio_channels ();
+ check_upmixers ();
+ check_incorrect_container ();
+ check_unusual_container ();
+ check_high_j2k_bandwidth ();
+ check_frame_rate ();
+ check_4k_3d ();
+ check_speed_up ();
+ check_vob ();
+ check_3d_in_2d ();
+ check_ffec_and_ffmc_in_smpte_feature ();
+ check_out_of_range_markers ();
+ check_subtitle_languages();
+ check_audio_language ();
+ check_8_or_16_audio_channels();
+
+ scan_content(film);
+
+ if (_long_subtitle && !_very_long_subtitle) {
+ hint (_("At least one of your subtitle lines has more than 52 characters. It is recommended to make each line 52 characters at most in length."));
+ } else if (_very_long_subtitle) {
+ hint (_("At least one of your subtitle lines has more than 79 characters. You should make each line 79 characters at most in length."));
+ }
+
+ bool ccap_xml_too_big = false;
+ bool ccap_mxf_too_big = false;
+ bool subs_mxf_too_big = false;
+
+ auto dcp_dir = film->dir("hints") / dcpomatic::get_process_id();
+ dcp::filesystem::remove_all(dcp_dir);
+
+ _writer->finish (film->dir("hints") / dcpomatic::get_process_id());
+
+ dcp::DCP dcp (dcp_dir);
+ dcp.read ();
+ DCPOMATIC_ASSERT (dcp.cpls().size() == 1);
+ for (auto reel: dcp.cpls()[0]->reels()) {
+ for (auto ccap: reel->closed_captions()) {
+ if (ccap->asset() && ccap->asset()->xml_as_string().length() > static_cast<size_t>(MAX_CLOSED_CAPTION_XML_SIZE - SIZE_SLACK) && !ccap_xml_too_big) {
+ hint (_(
+ "At least one of your closed caption files' XML part is larger than " MAX_CLOSED_CAPTION_XML_SIZE_TEXT
+ ". You should divide the DCP into shorter reels."
+ ));
+ ccap_xml_too_big = true;
+ }
+ if (subtitle_mxf_too_big(ccap->asset()) && !ccap_mxf_too_big) {
+ hint (_(
+ "At least one of your closed caption files is larger than " MAX_TEXT_MXF_SIZE_TEXT
+ " in total. You should divide the DCP into shorter reels."
+ ));
+ ccap_mxf_too_big = true;
+ }
+ }
+ if (reel->main_subtitle() && subtitle_mxf_too_big(reel->main_subtitle()->asset()) && !subs_mxf_too_big) {
+ hint (_(
+ "At least one of your subtitle files is larger than " MAX_TEXT_MXF_SIZE_TEXT " in total. "
+ "You should divide the DCP into shorter reels."
+ ));
+ subs_mxf_too_big = true;
+ }
+ }
+ dcp::filesystem::remove_all(dcp_dir);
+
+ emit (bind(boost::ref(Finished)));
+}
+catch (boost::thread_interrupted)
+{
+ /* The Hints object is being destroyed before it has finished, so just give up */
+}
+catch (...)
+{
+ store_current ();
+}
+
+
+void
+Hints::hint (string h)
+{
+ emit(bind(boost::ref(Hint), h));
+}
+
+
+void
+Hints::audio (shared_ptr<AudioBuffers> audio, DCPTime time)
+{
+ _analyser.analyse (audio, time);
+}
+
+
+void
+Hints::text (PlayerText text, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
+{
+ _writer->write (text, type, track, period);
+
+ switch (type) {
+ case TextType::CLOSED_CAPTION:
+ closed_caption (text, period);
+ break;
+ case TextType::OPEN_SUBTITLE:
+ open_subtitle (text, period);
+ break;
+ default:
+ break;
+ }
+}
+
+
+void
+Hints::closed_caption (PlayerText text, DCPTimePeriod period)
+{
+ int lines = text.string.size();
+ for (auto i: text.string) {
+ if (utf8_strlen(i.text()) > MAX_CLOSED_CAPTION_LENGTH) {
+ ++lines;
+ if (!_long_ccap) {
+ _long_ccap = true;
+ hint (
+ String::compose(
+ "At least one of your closed caption lines has more than %1 characters. "
+ "It is advisable to make each line %1 characters at most in length.",
+ MAX_CLOSED_CAPTION_LENGTH,
+ MAX_CLOSED_CAPTION_LENGTH)
+ );
+ }
+ }
+ }
+
+ if (!_too_many_ccap_lines && lines > MAX_CLOSED_CAPTION_LINES) {
+ hint (String::compose(_("Some of your closed captions span more than %1 lines, so they will be truncated."), MAX_CLOSED_CAPTION_LINES));
+ _too_many_ccap_lines = true;
+ }
+
+ /* XXX: maybe overlapping closed captions (i.e. different languages) are OK with Interop? */
+ if (film()->interop() && !_overlap_ccap && _last_ccap && _last_ccap->overlap(period)) {
+ _overlap_ccap = true;
+ hint (_("You have overlapping closed captions, which are not allowed in Interop DCPs. Change your DCP standard to SMPTE."));
+ }
+
+ _last_ccap = period;
+}
+
+
+void
+Hints::open_subtitle (PlayerText text, DCPTimePeriod period)
+{
+ if (period.from < DCPTime::from_seconds(4) && !_early_subtitle) {
+ _early_subtitle = true;
+ hint (_("It is advisable to put your first subtitle at least 4 seconds after the start of the DCP to make sure it is seen."));
+ }
+
+ int const vfr = film()->video_frame_rate ();
+
+ if (period.duration().frames_round(vfr) < 15 && !_short_subtitle) {
+ _short_subtitle = true;
+ hint (_("At least one of your subtitles lasts less than 15 frames. It is advisable to make each subtitle at least 15 frames long."));
+ }
+
+ if (_last_subtitle && DCPTime(period.from - _last_subtitle->to).frames_round(vfr) < 2 && !_subtitles_too_close) {
+ _subtitles_too_close = true;
+ hint (_("At least one of your subtitles starts less than 2 frames after the previous one. It is advisable to make the gap between subtitles at least 2 frames."));
+ }
+
+ struct VPos
+ {
+ public:
+ dcp::VAlign align;
+ float position;
+
+ bool operator<(VPos const& other) const {
+ if (static_cast<int>(align) != static_cast<int>(other.align)) {
+ return static_cast<int>(align) < static_cast<int>(other.align);
+ }
+ return position < other.position;
+ }
+ };
+
+ /* This is rather an approximate way to count distinct lines, but I guess it will do;
+ * to make it better we need to take into account font metrics, and the SMPTE alignment
+ * debacle, and so on.
+ */
+ std::set<VPos> lines;
+ for (auto const& line: text.string) {
+ lines.insert({ line.v_align(), line.v_position() });
+ }
+
+ if (lines.size() > 3 && !_too_many_subtitle_lines) {
+ _too_many_subtitle_lines = true;
+ hint (_("At least one of your subtitles has more than 3 lines. It is advisable to use no more than 3 lines."));
+ }
+
+ size_t longest_line = 0;
+ for (auto const& i: text.string) {
+ longest_line = max (longest_line, i.text().length());
+ }
+
+ if (longest_line > 52) {
+ _long_subtitle = true;
+ }
+
+ if (longest_line > 79) {
+ _very_long_subtitle = true;
+ }
+
+ _last_subtitle = period;
+}
+
+
+void
+Hints::check_ffec_and_ffmc_in_smpte_feature ()
+{
+ auto f = film();
+ if (!f->interop() && f->dcp_content_type()->libdcp_kind() == dcp::ContentKind::FEATURE && (!f->marker(dcp::Marker::FFEC) || !f->marker(dcp::Marker::FFMC))) {
+ hint (_("SMPTE DCPs with the type FTR (feature) should have markers for the first frame of end credits (FFEC) and the first frame of moving credits (FFMC). You should add these markers using the 'Markers' button in the \"DCP\" tab."));
+ }
+}
+
+
+void
+Hints::join ()
+{
+ _thread.join ();
+}
+
+
+void
+Hints::check_subtitle_languages()
+{
+ for (auto i: film()->content()) {
+ for (auto j: i->text) {
+ if (j->use() && j->type() == TextType::OPEN_SUBTITLE && !j->language()) {
+ hint (_("At least one piece of subtitle content has no specified language. "
+ "It is advisable to set the language for each piece of subtitle content "
+ "in the \"Content→Timed text\" or \"Content→Open subtitles\" tab."));
+ return;