#include "exceptions.h"
#include "compose.hpp"
#include "raw_convert.h"
+#include "reel_markers_asset.h"
#include "smpte_subtitle_asset.h"
#include <xercesc/util/PlatformUtils.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
using std::cout;
using std::map;
using std::max;
+using std::set;
using std::shared_ptr;
using std::make_shared;
using boost::optional;
static void
-parse (XercesDOMParser& parser, std::string xml)
+parse (XercesDOMParser& parser, string xml)
{
xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
parser.parse(buf);
stage ("Checking sound asset metadata", asset->file());
verify_language_tag (asset->language(), notes);
+ if (asset->sampling_rate() != 48000) {
+ notes.push_back (
+ VerificationNote(
+ VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_SOUND_FRAME_RATE, *asset->file()
+ )
+ );
+ }
}
if (reel_asset->language()) {
verify_language_tag (*reel_asset->language(), notes);
}
+
+ if (!reel_asset->entry_point()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_SUBTITLE_ENTRY_POINT });
+ } else if (reel_asset->entry_point().get()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO });
+ }
}
if (reel_asset->language()) {
verify_language_tag (*reel_asset->language(), notes);
}
+
+ if (!reel_asset->entry_point()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT });
+ } else if (reel_asset->entry_point().get()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO });
+ }
}
vector<shared_ptr<dcp::Reel>> reels,
optional<int> picture_frame_rate,
vector<VerificationNote>& notes,
- std::function<std::string (shared_ptr<dcp::Reel>)> xml,
+ std::function<bool (shared_ptr<dcp::Reel>)> check,
+ std::function<string (shared_ptr<dcp::Reel>)> xml,
std::function<int64_t (shared_ptr<dcp::Reel>)> duration
)
{
};
for (auto i = 0U; i < reels.size(); ++i) {
+ if (!check(reels[i])) {
+ continue;
+ }
+
/* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
* read in by libdcp's parser.
*/
}
+struct LinesCharactersResult
+{
+ bool warning_length_exceeded = false;
+ bool error_length_exceeded = false;
+ bool line_count_exceeded = false;
+};
+
+
+static
+void
+check_text_lines_and_characters (
+ shared_ptr<SubtitleAsset> asset,
+ int warning_length,
+ int error_length,
+ LinesCharactersResult* result
+ )
+{
+ class Event
+ {
+ public:
+ Event (dcp::Time time_, float position_, int characters_)
+ : time (time_)
+ , position (position_)
+ , characters (characters_)
+ {}
+
+ Event (dcp::Time time_, shared_ptr<Event> start_)
+ : time (time_)
+ , start (start_)
+ {}
+
+ dcp::Time time;
+ int position; //< position from 0 at top of screen to 100 at bottom
+ int characters;
+ shared_ptr<Event> start;
+ };
+
+ vector<shared_ptr<Event>> events;
+
+ auto position = [](shared_ptr<const SubtitleString> sub) {
+ switch (sub->v_align()) {
+ case VALIGN_TOP:
+ return lrintf(sub->v_position() * 100);
+ case VALIGN_CENTER:
+ return lrintf((0.5f + sub->v_position()) * 100);
+ case VALIGN_BOTTOM:
+ return lrintf((1.0f - sub->v_position()) * 100);
+ }
+
+ return 0L;
+ };
+
+ for (auto j: asset->subtitles()) {
+ auto text = dynamic_pointer_cast<const SubtitleString>(j);
+ if (text) {
+ auto in = make_shared<Event>(text->in(), position(text), text->text().length());
+ events.push_back(in);
+ events.push_back(make_shared<Event>(text->out(), in));
+ }
+ }
+
+ std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
+ return a->time < b->time;
+ });
+
+ map<int, int> current;
+ for (auto i: events) {
+ if (current.size() > 3) {
+ result->line_count_exceeded = true;
+ }
+ for (auto j: current) {
+ if (j.second >= warning_length) {
+ result->warning_length_exceeded = true;
+ }
+ if (j.second >= error_length) {
+ result->error_length_exceeded = true;
+ }
+ }
+
+ if (i->start) {
+ /* end of a subtitle */
+ DCP_ASSERT (current.find(i->start->position) != current.end());
+ if (current[i->start->position] == i->start->characters) {
+ current.erase(i->start->position);
+ } else {
+ current[i->start->position] -= i->start->characters;
+ }
+ } else {
+ /* start of a subtitle */
+ if (current.find(i->position) == current.end()) {
+ current[i->position] = i->characters;
+ } else {
+ current[i->position] += i->characters;
+ }
+ }
+ }
+}
+
+
static
void
check_text_timing (vector<shared_ptr<dcp::Reel>> reels, vector<VerificationNote>& notes)
if (reels[0]->main_subtitle()) {
check_text_timing (reels, picture_frame_rate, notes,
+ [](shared_ptr<dcp::Reel> reel) {
+ return static_cast<bool>(reel->main_subtitle());
+ },
[](shared_ptr<dcp::Reel> reel) {
return reel->main_subtitle()->asset()->raw_xml();
},
for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
check_text_timing (reels, picture_frame_rate, notes,
+ [i](shared_ptr<dcp::Reel> reel) {
+ return i < reel->closed_captions().size();
+ },
[i](shared_ptr<dcp::Reel> reel) {
return reel->closed_captions()[i]->asset()->raw_xml();
},
}
+void
+check_extension_metadata (shared_ptr<dcp::CPL> cpl, vector<VerificationNote>& notes)
+{
+ DCP_ASSERT (cpl->file());
+ cxml::Document doc ("CompositionPlaylist");
+ doc.read_file (cpl->file().get());
+
+ auto missing = false;
+ string malformed;
+
+ if (auto reel_list = doc.node_child("ReelList")) {
+ auto reels = reel_list->node_children("Reel");
+ if (!reels.empty()) {
+ if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
+ if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
+ if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
+ missing = true;
+ for (auto extension: extension_list->node_children("ExtensionMetadata")) {
+ if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
+ continue;
+ }
+ missing = false;
+ if (auto name = extension->optional_node_child("Name")) {
+ if (name->content() != "Application") {
+ malformed = "<Name> should be 'Application'";
+ }
+ }
+ if (auto property_list = extension->optional_node_child("PropertyList")) {
+ if (auto property = property_list->optional_node_child("Property")) {
+ if (auto name = property->optional_node_child("Name")) {
+ if (name->content() != "DCP Constraints Profile") {
+ malformed = "<Name> property should be 'DCP Constraints Profile'";
+ }
+ }
+ if (auto value = property->optional_node_child("Value")) {
+ if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
+ malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
+ }
+ }
+ }
+ }
+ }
+ } else {
+ missing = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (missing) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_EXTENSION_METADATA});
+ } else if (!malformed.empty()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::INVALID_EXTENSION_METADATA, malformed});
+ }
+}
+
+
vector<VerificationNote>
dcp::verify (
vector<boost::filesystem::path> directories,
}
if (cpl->release_territory()) {
- verify_language_tag (cpl->release_territory().get(), notes);
+ if (!cpl->release_territory_scope() || cpl->release_territory_scope().get() != "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata#scope/release-territory/UNM49") {
+ auto terr = cpl->release_territory().get();
+ /* Must be a valid region tag, or "001" */
+ try {
+ LanguageTag::RegionSubtag test (terr);
+ } catch (...) {
+ if (terr != "001") {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::BAD_LANGUAGE, terr});
+ }
+ }
+ }
+ }
+
+ if (dcp->standard() == dcp::SMPTE) {
+ if (!cpl->annotation_text()) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL));
+ } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT));
+ }
}
/* Check that the CPL's hash corresponds to the PKL */
}
}
+ /* set to true if any reel has a MainSubtitle */
+ auto have_main_subtitle = false;
+ /* set to true if any reel has no MainSubtitle */
+ auto have_no_main_subtitle = false;
+ /* fewest number of closed caption assets seen in a reel */
+ size_t fewest_closed_captions = SIZE_MAX;
+ /* most number of closed caption assets seen in a reel */
+ size_t most_closed_captions = 0;
+ map<Marker, Time> markers_seen;
+
for (auto reel: cpl->reels()) {
stage ("Checking reel", optional<boost::filesystem::path>());
if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id()));
}
+ auto mxf = dynamic_pointer_cast<ReelMXF>(i);
+ if (mxf && !mxf->hash()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_HASH, i->id()});
+ }
+ }
+
+ if (dcp->standard() == dcp::SMPTE) {
+ boost::optional<int64_t> duration;
+ for (auto i: reel->assets()) {
+ if (!duration) {
+ duration = i->actual_duration();
+ } else if (*duration != i->actual_duration()) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISMATCHED_ASSET_DURATION, i->id()));
+ break;
+ }
+ }
}
if (reel->main_picture()) {
if (reel->main_subtitle()->asset_ref().resolved()) {
verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
}
+ have_main_subtitle = true;
+ } else {
+ have_no_main_subtitle = true;
}
for (auto i: reel->closed_captions()) {
verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
}
}
+
+ if (reel->main_markers()) {
+ for (auto const& i: reel->main_markers()->get()) {
+ markers_seen.insert (i);
+ }
+ }
+
+ fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
+ most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
}
if (dcp->standard() == dcp::SMPTE) {
+
+ if (have_main_subtitle && have_no_main_subtitle) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS});
+ }
+
+ if (fewest_closed_captions != most_closed_captions) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER});
+ }
+
+ if (cpl->content_kind() == FEATURE) {
+ if (markers_seen.find(dcp::Marker::FFEC) == markers_seen.end()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFEC_IN_FEATURE});
+ }
+ if (markers_seen.find(dcp::Marker::FFMC) == markers_seen.end()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_FFMC_IN_FEATURE});
+ }
+ }
+
+ auto ffoc = markers_seen.find(dcp::Marker::FFOC);
+ if (ffoc == markers_seen.end()) {
+ notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_FFOC});
+ } else if (ffoc->second.e != 1) {
+ notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_FFOC});
+ }
+
+ auto lfoc = markers_seen.find(dcp::Marker::LFOC);
+ if (lfoc == markers_seen.end()) {
+ notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::MISSING_LFOC});
+ } else if (lfoc->second.as_editable_units(lfoc->second.tcr) != (cpl->reels().back()->duration() - 1)) {
+ notes.push_back ({VerificationNote::VERIFY_WARNING, VerificationNote::INCORRECT_LFOC});
+ }
+
check_text_timing (cpl->reels(), notes);
+
+ LinesCharactersResult result;
+ for (auto reel: cpl->reels()) {
+ if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
+ check_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
+ }
+ }
+
+ if (result.line_count_exceeded) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::TOO_MANY_SUBTITLE_LINES));
+ }
+ if (result.error_length_exceeded) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_TOO_LONG));
+ } else if (result.warning_length_exceeded) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED));
+ }
+
+ result = LinesCharactersResult();
+ for (auto reel: cpl->reels()) {
+ for (auto i: reel->closed_captions()) {
+ if (i->asset()) {
+ check_text_lines_and_characters (i->asset(), 32, 32, &result);
+ }
+ }
+ }
+
+ if (result.line_count_exceeded) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES));
+ }
+ if (result.error_length_exceeded) {
+ notes.push_back (VerificationNote(VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG));
+ }
+
+ if (!cpl->full_content_title_text()) {
+ /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
+ * can use it as a proxy for CompositionMetadataAsset's existence.
+ */
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CPL_METADATA});
+ } else if (!cpl->version_number()) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::MISSING_CPL_METADATA_VERSION_NUMBER});
+ }
+
+ check_extension_metadata (cpl, notes);
+
+ if (cpl->encrypted()) {
+ cxml::Document doc ("CompositionPlaylist");
+ DCP_ASSERT (cpl->file());
+ doc.read_file (cpl->file().get());
+ if (!doc.optional_node_child("Signature")) {
+ notes.push_back ({VerificationNote::VERIFY_BV21_ERROR, VerificationNote::CPL_WITH_ENCRYPTED_CONTENT_NOT_SIGNED, cpl->file().get()});
+ }
+ }
}
}
return "At least one subtitle is less than the minimum of 15 frames suggested by Bv2.1";
case dcp::VerificationNote::SUBTITLE_TOO_CLOSE:
return "At least one pair of subtitles are separated by less than the the minimum of 2 frames suggested by Bv2.1";
+ case dcp::VerificationNote::TOO_MANY_SUBTITLE_LINES:
+ return "There are more than 3 subtitle lines in at least one place in the DCP, which Bv2.1 advises against.";
+ case dcp::VerificationNote::SUBTITLE_LINE_LONGER_THAN_RECOMMENDED:
+ return "There are more than 52 characters in at least one subtitle line, which Bv2.1 advises against.";
+ case dcp::VerificationNote::SUBTITLE_LINE_TOO_LONG:
+ return "There are more than 79 characters in at least one subtitle line, which Bv2.1 strongly advises against.";
+ case dcp::VerificationNote::TOO_MANY_CLOSED_CAPTION_LINES:
+ return "There are more than 3 closed caption lines in at least one place, which is disallowed by Bv2.1";
+ case dcp::VerificationNote::CLOSED_CAPTION_LINE_TOO_LONG:
+ return "There are more than 32 characters in at least one closed caption line, which is disallowed by Bv2.1";
+ case dcp::VerificationNote::INVALID_SOUND_FRAME_RATE:
+ return "A sound asset has a sampling rate other than 48kHz, which is disallowed by Bv2.1";
+ case dcp::VerificationNote::MISSING_ANNOTATION_TEXT_IN_CPL:
+ return "The CPL has no <AnnotationText> tag, which is required by Bv2.1";
+ case dcp::VerificationNote::CPL_ANNOTATION_TEXT_DIFFERS_FROM_CONTENT_TITLE_TEXT:
+ return "The CPL's <AnnotationText> differs from its <ContentTitleText>, which Bv2.1 advises against.";
+ case dcp::VerificationNote::MISMATCHED_ASSET_DURATION:
+ return "All assets in a reel do not have the same duration, which is required by Bv2.1";
+ case dcp::VerificationNote::MAIN_SUBTITLE_NOT_IN_ALL_REELS:
+ return "At least one reel contains a subtitle asset, but some reel(s) do not";
+ case dcp::VerificationNote::CLOSED_CAPTION_ASSET_COUNTS_DIFFER:
+ return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
+ case dcp::VerificationNote::MISSING_SUBTITLE_ENTRY_POINT:
+ return "Subtitle assets must have an <EntryPoint> tag.";
+ case dcp::VerificationNote::SUBTITLE_ENTRY_POINT_NON_ZERO:
+ return "Subtitle assets must have an <EntryPoint> of 0.";
+ case dcp::VerificationNote::MISSING_CLOSED_CAPTION_ENTRY_POINT:
+ return "Closed caption assets must have an <EntryPoint> tag.";
+ case dcp::VerificationNote::CLOSED_CAPTION_ENTRY_POINT_NON_ZERO:
+ return "Closed caption assets must have an <EntryPoint> of 0.";
+ case dcp::VerificationNote::MISSING_HASH:
+ return String::compose("An asset is missing a <Hash> tag: %1", note.note().get());
+ case dcp::VerificationNote::MISSING_FFEC_IN_FEATURE:
+ return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
+ case dcp::VerificationNote::MISSING_FFMC_IN_FEATURE:
+ return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
+ case dcp::VerificationNote::MISSING_FFOC:
+ return "There should be a FFOC (first frame of content) marker";
+ case dcp::VerificationNote::MISSING_LFOC:
+ return "There should be a LFOC (last frame of content) marker";
+ case dcp::VerificationNote::INCORRECT_FFOC:
+ return "The FFOC marker should bet set to 1";
+ case dcp::VerificationNote::INCORRECT_LFOC:
+ return "The LFOC marker should be set to 1 less than the duration of the last reel";
+ case dcp::VerificationNote::MISSING_CPL_METADATA:
+ return "There should be a <CompositionMetadataAsset> tag";
+ case dcp::VerificationNote::MISSING_CPL_METADATA_VERSION_NUMBER:
+ return "The CPL metadata must contain a <VersionNumber>";
+ case dcp::VerificationNote::MISSING_EXTENSION_METADATA:
+ return "The CPL metadata must contain <ExtensionMetadata>";
+ case dcp::VerificationNote::INVALID_EXTENSION_METADATA:
+ return String::compose("The <ExtensionMetadata> is malformed in some way: %1", note.note().get());
+ case dcp::VerificationNote::CPL_WITH_ENCRYPTED_CONTENT_NOT_SIGNED:
+ return String::compose("The CPL %1, which has encrypted content, is not signed", note.file()->filename());
}
return "";