#include "raw_convert.h"
#include "reel.h"
#include "reel_closed_caption_asset.h"
+#include "reel_interop_subtitle_asset.h"
#include "reel_markers_asset.h"
#include "reel_picture_asset.h"
#include "reel_sound_asset.h"
+#include "reel_smpte_subtitle_asset.h"
#include "reel_subtitle_asset.h"
#include "smpte_subtitle_asset.h"
#include "stereo_picture_asset.h"
using std::cout;
using std::map;
using std::max;
-using std::set;
using std::shared_ptr;
using std::make_shared;
using boost::optional;
class DCPErrorHandler : public ErrorHandler
{
public:
- void warning(const SAXParseException& e)
+ void warning(const SAXParseException& e) override
{
maybe_add (XMLValidationError(e));
}
- void error(const SAXParseException& e)
+ void error(const SAXParseException& e) override
{
maybe_add (XMLValidationError(e));
}
- void fatalError(const SAXParseException& e)
+ void fatalError(const SAXParseException& e) override
{
maybe_add (XMLValidationError(e));
}
- void resetErrors() {
+ void resetErrors() override {
_errors.clear ();
}
add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
}
- InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
+ InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
{
if (!system_id) {
return 0;
for (int64_t i = 0; i < duration; ++i) {
auto frame = reader->get_frame (i);
biggest_frame = max(biggest_frame, frame->size());
- vector<VerificationNote> j2k_notes;
- verify_j2k (frame, j2k_notes);
- check_and_add (j2k_notes);
+ if (!mono_asset->encrypted() || mono_asset->key()) {
+ vector<VerificationNote> j2k_notes;
+ verify_j2k (frame, j2k_notes);
+ check_and_add (j2k_notes);
+ }
progress (float(i) / duration);
}
} else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
for (int64_t i = 0; i < duration; ++i) {
auto frame = reader->get_frame (i);
biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
- vector<VerificationNote> j2k_notes;
- verify_j2k (frame->left(), j2k_notes);
- verify_j2k (frame->right(), j2k_notes);
- check_and_add (j2k_notes);
+ if (!stereo_asset->encrypted() || mono_asset->key()) {
+ vector<VerificationNote> j2k_notes;
+ verify_j2k (frame->left(), j2k_notes);
+ verify_j2k (frame->right(), j2k_notes);
+ check_and_add (j2k_notes);
+ }
progress (float(i) / duration);
}
stage ("Checking sound asset metadata", asset->file());
- verify_language_tag (asset->language(), notes);
+ if (auto lang = asset->language()) {
+ verify_language_tag (*lang, notes);
+ }
if (asset->sampling_rate() != 48000) {
notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
}
void
verify_smpte_timed_text_asset (
shared_ptr<const SMPTESubtitleAsset> asset,
+ optional<int64_t> reel_asset_duration,
vector<VerificationNote>& notes
)
{
} else if (asset->start_time() != Time()) {
notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
}
+
+ if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
+ notes.push_back (
+ {
+ VerificationNote::Type::BV21_ERROR,
+ VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
+ String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
+ asset->file().get()
+ });
+ }
}
notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
}
}
+
+ DCP_ASSERT (asset->resource_id());
+ auto xml_id = asset->xml_id();
+ if (xml_id) {
+ if (asset->resource_id().get() != xml_id) {
+ notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
+ }
+
+ if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
+ notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
+ }
+ } else {
+ notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+ }
}
static void
verify_subtitle_asset (
shared_ptr<const SubtitleAsset> asset,
+ optional<int64_t> reel_asset_duration,
function<void (string, optional<boost::filesystem::path>)> stage,
boost::filesystem::path xsd_dtd_directory,
vector<VerificationNote>& notes,
/* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
* gets passed through libdcp which may clean up and therefore hide errors.
*/
- validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
+ if (asset->raw_xml()) {
+ validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
+ } else {
+ notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+ }
auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
if (smpte) {
- verify_smpte_timed_text_asset (smpte, notes);
+ verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
verify_smpte_subtitle_asset (smpte, notes, state);
}
}
static void
verify_closed_caption_asset (
shared_ptr<const SubtitleAsset> asset,
+ optional<int64_t> reel_asset_duration,
function<void (string, optional<boost::filesystem::path>)> stage,
boost::filesystem::path xsd_dtd_directory,
vector<VerificationNote>& notes
/* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
* gets passed through libdcp which may clean up and therefore hide errors.
*/
- validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
+ auto raw_xml = asset->raw_xml();
+ if (raw_xml) {
+ validate_xml (*raw_xml, xsd_dtd_directory, notes);
+ if (raw_xml->size() > 256 * 1024) {
+ notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
+ }
+ } else {
+ notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+ }
auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
if (smpte) {
- verify_smpte_timed_text_asset (smpte, notes);
- }
-
- if (asset->raw_xml().size() > 256 * 1024) {
- notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(asset->raw_xml().size()), *asset->file()});
+ verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
}
}
+/** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes */
static
void
-verify_text_timing (
+verify_text_details (
vector<shared_ptr<Reel>> reels,
int edit_rate,
vector<VerificationNote>& notes,
std::function<bool (shared_ptr<Reel>)> check,
- std::function<string (shared_ptr<Reel>)> xml,
+ std::function<optional<string> (shared_ptr<Reel>)> xml,
std::function<int64_t (shared_ptr<Reel>)> duration
)
{
auto too_close = false;
auto too_early = false;
auto reel_overlap = false;
+ auto empty_text = false;
/* current reel start time (in editable units) */
int64_t reel_offset = 0;
std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool)> parse;
- parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
+ parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset](cxml::ConstNodePtr node, optional<int> tcr, optional<Time> start_time, int er, bool first_reel) {
if (node->name() == "Subtitle") {
Time in (node->string_attribute("TimeIn"), tcr);
if (start_time) {
}
}
last_out = reel_offset + out.as_editable_units_floor(er);
- } else {
- for (auto i: node->node_children()) {
- parse(i, tcr, start_time, er, first_reel);
+ } else if (node->name() == "Text") {
+ std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
+ if (!node->content().empty()) {
+ return true;
+ }
+ for (auto i: node->node_children()) {
+ if (node_has_content(i)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ if (!node_has_content(node)) {
+ empty_text = true;
}
}
+
+ for (auto i: node->node_children()) {
+ parse(i, tcr, start_time, er, first_reel);
+ }
};
for (auto i = 0U; i < reels.size(); ++i) {
continue;
}
+ auto reel_xml = xml(reels[i]);
+ if (!reel_xml) {
+ notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+ 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.
*/
optional<Time> start_time;
try {
doc = make_shared<cxml::Document>("SubtitleReel");
- doc->read_string (xml(reels[i]));
+ doc->read_string (*reel_xml);
tcr = doc->number_child<int>("TimeCodeRate");
auto start_time_string = doc->optional_string_child("StartTime");
if (start_time_string) {
}
} catch (...) {
doc = make_shared<cxml::Document>("DCSubtitle");
- doc->read_string (xml(reels[i]));
+ doc->read_string (*reel_xml);
}
parse (doc, tcr, start_time, edit_rate, i == 0);
auto end = reel_offset + duration(reels[i]);
VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
});
}
+
+ if (empty_text) {
+ notes.push_back ({
+ VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
+ });
+ }
+}
+
+
+static
+void
+verify_closed_caption_details (
+ vector<shared_ptr<Reel>> reels,
+ vector<VerificationNote>& notes
+ )
+{
+ std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
+ find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
+ for (auto i: node->node_children()) {
+ if (i->name() == "Text") {
+ text_or_image.push_back (i);
+ } else {
+ find_text_or_image (i, text_or_image);
+ }
+ }
+ };
+
+ auto mismatched_valign = false;
+ auto incorrect_order = false;
+
+ std::function<void (cxml::ConstNodePtr)> parse;
+ parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
+ if (node->name() == "Subtitle") {
+ vector<cxml::ConstNodePtr> text_or_image;
+ find_text_or_image (node, text_or_image);
+ optional<string> last_valign;
+ optional<float> last_vpos;
+ for (auto i: text_or_image) {
+ auto valign = i->optional_string_attribute("VAlign");
+ if (!valign) {
+ valign = i->optional_string_attribute("Valign").get_value_or("center");
+ }
+ auto vpos = i->optional_number_attribute<float>("VPosition");
+ if (!vpos) {
+ vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
+ }
+
+ if (last_valign) {
+ if (*last_valign != valign) {
+ mismatched_valign = true;
+ }
+ }
+ last_valign = valign;
+
+ if (!mismatched_valign) {
+ if (last_vpos) {
+ if (*last_valign == "top" || *last_valign == "center") {
+ if (*vpos < *last_vpos) {
+ incorrect_order = true;
+ }
+ } else {
+ if (*vpos > *last_vpos) {
+ incorrect_order = true;
+ }
+ }
+ }
+ last_vpos = vpos;
+ }
+ }
+ }
+
+ for (auto i: node->node_children()) {
+ parse(i);
+ }
+ };
+
+ for (auto reel: reels) {
+ for (auto ccap: reel->closed_captions()) {
+ auto reel_xml = ccap->asset()->raw_xml();
+ if (!reel_xml) {
+ notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
+ 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.
+ */
+
+ shared_ptr<cxml::Document> doc;
+ optional<int> tcr;
+ optional<Time> start_time;
+ try {
+ doc = make_shared<cxml::Document>("SubtitleReel");
+ doc->read_string (*reel_xml);
+ } catch (...) {
+ doc = make_shared<cxml::Document>("DCSubtitle");
+ doc->read_string (*reel_xml);
+ }
+ parse (doc);
+ }
+ }
+
+ if (mismatched_valign) {
+ notes.push_back ({
+ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
+ });
+ }
+
+ if (incorrect_order) {
+ notes.push_back ({
+ VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
+ });
+ }
}
result->line_count_exceeded = true;
}
for (auto j: current) {
- if (j.second >= warning_length) {
+ if (j.second > warning_length) {
result->warning_length_exceeded = true;
}
- if (j.second >= error_length) {
+ if (j.second > error_length) {
result->error_length_exceeded = true;
}
}
static
void
-verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
+verify_text_details (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
{
if (reels.empty()) {
return;
}
if (reels[0]->main_subtitle()) {
- verify_text_timing (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
+ verify_text_details (reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
[](shared_ptr<Reel> reel) {
return static_cast<bool>(reel->main_subtitle());
},
[](shared_ptr<Reel> reel) {
- return reel->main_subtitle()->asset()->raw_xml();
+ auto interop = dynamic_pointer_cast<ReelInteropSubtitleAsset>(reel->main_subtitle());
+ if (interop) {
+ return interop->asset()->raw_xml();
+ }
+ auto smpte = dynamic_pointer_cast<ReelSMPTESubtitleAsset>(reel->main_subtitle());
+ DCP_ASSERT (smpte);
+ return smpte->asset()->raw_xml();
},
[](shared_ptr<Reel> reel) {
return reel->main_subtitle()->actual_duration();
}
for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
- verify_text_timing (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
+ verify_text_details (reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
[i](shared_ptr<Reel> reel) {
return i < reel->closed_captions().size();
},
}
);
}
+
+ verify_closed_caption_details (reels, notes);
}
for (auto i: dcp->cpls()) {
for (auto j: i->reel_file_assets()) {
if (j->asset_ref().resolved()) {
- /* It's a bit surprising / broken but Interop subtitle assets are represented
- * in reels by ReelSubtitleAsset which inherits ReelFileAsset, so it's possible for
- * ReelFileAssets to have assets which are not MXFs.
- */
- if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
- if (asset->encrypted()) {
- encrypted.push_back(j->asset_ref().id());
- }
+ auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
+ if (mxf && mxf->encrypted()) {
+ encrypted.push_back(j->asset_ref().id());
}
}
}
stage ("Checking DCP", dcp->directory());
bool carry_on = true;
try {
- dcp->read (¬es);
+ dcp->read (¬es, true);
} catch (MissingAssetmapError& e) {
notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
carry_on = false;
notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
}
auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
- if (file_asset && !file_asset->hash()) {
+ if (i->encryptable() && !file_asset->hash()) {
notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
}
}
if (reel->main_subtitle()) {
verify_main_subtitle_reel (reel->main_subtitle(), notes);
if (reel->main_subtitle()->asset_ref().resolved()) {
- verify_subtitle_asset (reel->main_subtitle()->asset(), stage, *xsd_dtd_directory, notes, state);
+ verify_subtitle_asset (reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, *xsd_dtd_directory, notes, state);
}
have_main_subtitle = true;
} else {
for (auto i: reel->closed_captions()) {
verify_closed_caption_reel (i, notes);
if (i->asset_ref().resolved()) {
- verify_closed_caption_asset (i->asset(), stage, *xsd_dtd_directory, notes);
+ verify_closed_caption_asset (i->asset(), i->duration(), stage, *xsd_dtd_directory, notes);
}
}
for (auto const& i: reel->main_markers()->get()) {
markers_seen.insert (i);
}
+ if (reel->main_markers()->entry_point()) {
+ notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
+ }
+ if (reel->main_markers()->duration()) {
+ notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
+ }
}
fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
}
- verify_text_timing (cpl->reels(), notes);
+ verify_text_details (cpl->reels(), notes);
if (dcp->standard() == Standard::SMPTE) {
return String::compose("The instantaneous bit rate of the picture asset %1 is close to the limit of 250Mbit/s in at least one place.", note.file()->filename());
case VerificationNote::Code::EXTERNAL_ASSET:
return String::compose("The asset %1 that this DCP refers to is not included in the DCP. It may be a VF.", note.note().get());
+ case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
+ return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
case VerificationNote::Code::INVALID_STANDARD:
return "This DCP does not use the SMPTE standard.";
case VerificationNote::Code::INVALID_LANGUAGE:
return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
return "No TLM marker was found in a JPEG2000 codestream.";
+ case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
+ return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
+ case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
+ return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
+ case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
+ {
+ vector<string> parts;
+ boost::split (parts, note.note().get(), boost::is_any_of(" "));
+ DCP_ASSERT (parts.size() == 2);
+ return String::compose("The reel duration of some timed text (%1) is not the same as the ContainerDuration of its MXF (%2).", parts[0], parts[1]);
+ }
+ case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
+ return "Some aspect of this DCP could not be checked because it is encrypted.";
+ case VerificationNote::Code::EMPTY_TEXT:
+ return "There is an empty <Text> node in a subtitle or closed caption.";
+ case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
+ return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
+ case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
+ return "Some closed captions are not listed in the order of their vertical position.";
+ case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
+ return "There is an <EntryPoint> node inside a <MainMarkers>.";
+ case VerificationNote::Code::UNEXPECTED_DURATION:
+ return "There is an <Duration> node inside a <MainMarkers>.";
}
return "";
}
+bool
+dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
+{
+ if (a.type() != b.type()) {
+ return a.type() < b.type();
+ }
+
+ if (a.code() != b.code()) {
+ return a.code() < b.code();
+ }
+
+ if (a.note() != b.note()) {
+ return a.note().get_value_or("") < b.note().get_value_or("");
+ }
+
+ if (a.file() != b.file()) {
+ return a.file().get_value_or("") < b.file().get_value_or("");
+ }
+
+ return a.line().get_value_or(0) < b.line().get_value_or(0);
+}
+
+
std::ostream&
dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
{