diff --git a/scripts/test-decode b/scripts/test-decode index fc05f4ca4..cb5883c8c 100755 --- a/scripts/test-decode +++ b/scripts/test-decode @@ -121,11 +121,12 @@ def run_ld_process_vbi(args): def run_ld_export_metadata(args): """Run ld-export-metadata.""" - clean(args, ['.vits.csv', '.vbi.csv']) + clean(args, ['.vits.csv', '.vbi.csv', '.ffmetadata']) cmd = [src_dir + '/tools/ld-export-metadata/ld-export-metadata'] cmd += ['--vits-csv', args.output + '.vits.csv'] cmd += ['--vbi-csv', args.output + '.vbi.csv'] + cmd += ['--ffmetadata', args.output + '.ffmetadata'] cmd += [args.output + '.tbc.json'] run_command(cmd) diff --git a/tools/ld-export-metadata/ffmetadata.cpp b/tools/ld-export-metadata/ffmetadata.cpp new file mode 100644 index 000000000..2e2a2b5d2 --- /dev/null +++ b/tools/ld-export-metadata/ffmetadata.cpp @@ -0,0 +1,147 @@ +/************************************************************************ + + ffmetadata.cpp + + ld-export-metadata - Export JSON metadata into other formats + Copyright (C) 2019-2020 Adam Sampson + + This file is part of ld-decode-tools. + + ld-export-metadata 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 3 of the + License, or (at your option) any later version. + + This program 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, see . + +************************************************************************/ + +#include "ffmetadata.h" + +#include "vbidecoder.h" + +#include +#include +#include +#include +#include + +using std::set; +using std::vector; + +struct ChapterChange { + qint32 field; + qint32 chapter; +}; + +bool writeFfmetadata(LdDecodeMetaData &metaData, const QString &fileName) +{ + const auto videoParameters = metaData.getVideoParameters(); + + // We'll give positions using 0-based field indexes directly, rather than + // using the times encoded in the VBI, because we might be working with a + // capture of only part of a disc. Select the appropriate timebase to make + // this work. + const QString timeBase = videoParameters.isSourcePal ? "1/50" : "1001/60000"; + + // Scan through the fields in the input, collecting VBI information + vector chapterChanges; + set stopCodes; + qint32 chapter = -1; + qint32 firstFieldIndex = 0; + VbiDecoder vbiDecoder; + + for (qint32 fieldIndex = 0; fieldIndex < videoParameters.numberOfSequentialFields; fieldIndex++) { + // Get the (1-based) field + const auto field = metaData.getField(fieldIndex + 1); + + // Codes may be in either field; we want the index of the first + if (field.isFirstField) { + firstFieldIndex = fieldIndex; + } + + // Decode this field's VBI + const auto vbi = vbiDecoder.decode(field.vbi.vbiData[0], field.vbi.vbiData[1], field.vbi.vbiData[2]); + + if (vbi.chNo != -1 && vbi.chNo != chapter) { + // Chapter change + chapter = vbi.chNo; + chapterChanges.emplace_back(ChapterChange {firstFieldIndex, chapter}); + } + + if (vbi.picStop) { + // Stop code + stopCodes.insert(firstFieldIndex); + } + } + + // Add a dummy change at the end of the input, so we can get the length of + // the last chapter + chapterChanges.emplace_back(ChapterChange {videoParameters.numberOfSequentialFields, -1}); + + // Because chapter markers have no error detection, a corrupt marker will + // result in a spurious chapter change. Remove suspiciously short chapters. + // XXX This could be smarter for sequences like 1 1 1 1 *2 2 3* 2 2 2 2 + vector cleanChanges; + for (qint32 i = 0; i < static_cast(chapterChanges.size() - 1); i++) { + const auto &change = chapterChanges[i]; + const auto &nextChange = chapterChanges[i + 1]; + + if ((nextChange.field - change.field) < 10) { + // Chapters should be at least 30 tracks (= 60 or more fields) long. So + // this is too short -- drop it. + } else if ((!cleanChanges.empty()) && (change.chapter == cleanChanges.back().chapter)) { + // Change to the same chapter - drop + } else { + // Keep + cleanChanges.emplace_back(change); + } + } + + // Keep the dummy change + cleanChanges.emplace_back(chapterChanges.back()); + + // Open the output file + QFile file(fileName); + if (!file.open(QFile::WriteOnly | QFile::Text)) { + qDebug("writeFfmetadata: Could not open file for output"); + return false; + } + QTextStream stream(&file); + stream.setCodec("UTF-8"); + + // Write the header + stream << ";FFMETADATA1\n"; + + // Write the chapter changes, skipping the dummy one at the end + for (qint32 i = 0; i < static_cast(cleanChanges.size() - 1); i++) { + const auto &change = cleanChanges[i]; + const auto &nextChange = cleanChanges[i + 1]; + + stream << "\n"; + stream << "[CHAPTER]\n"; + stream << "TIMEBASE=" << timeBase << "\n"; + stream << "START=" << change.field << "\n"; + stream << "END=" << (nextChange.field - 1) << "\n"; + stream << "title=" << QString("Chapter %1").arg(change.chapter) << "\n"; + } + + if (!stopCodes.empty()) { + // Write the stop codes, as comments + // XXX Is there a way to represent these properly? + stream << "\n"; + for (qint32 field : stopCodes) { + stream << "; Stop code at " << field << "\n"; + } + } + + // Done! + file.close(); + return true; +} diff --git a/tools/ld-export-metadata/ffmetadata.h b/tools/ld-export-metadata/ffmetadata.h new file mode 100644 index 000000000..d10b5d586 --- /dev/null +++ b/tools/ld-export-metadata/ffmetadata.h @@ -0,0 +1,43 @@ +/************************************************************************ + + ffmetadata.h + + ld-export-metadata - Export JSON metadata into other formats + Copyright (C) 2020 Adam Sampson + + This file is part of ld-decode-tools. + + ld-export-metadata 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 3 of the + License, or (at your option) any later version. + + This program 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, see . + +************************************************************************/ + +#ifndef FFMETADATA_H +#define FFMETADATA_H + +#include + +#include "lddecodemetadata.h" + +/*! + Write an FFMETADATA1 file containing navigation information. + + This is FFmpeg's generic metadata format, and can be used to provide + metadata for chapter-supporting formats like Matroska. + Format description: + + Returns true on success, false on failure. +*/ +bool writeFfmetadata(LdDecodeMetaData &metaData, const QString &fileName); + +#endif diff --git a/tools/ld-export-metadata/ld-export-metadata.pro b/tools/ld-export-metadata/ld-export-metadata.pro index f7ac310f3..7744e3e5a 100644 --- a/tools/ld-export-metadata/ld-export-metadata.pro +++ b/tools/ld-export-metadata/ld-export-metadata.pro @@ -16,12 +16,14 @@ DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ csv.cpp \ + ffmetadata.cpp \ main.cpp \ ../library/tbc/lddecodemetadata.cpp \ ../library/tbc/vbidecoder.cpp HEADERS += \ csv.h \ + ffmetadata.h \ ../library/tbc/lddecodemetadata.h \ ../library/tbc/vbidecoder.h diff --git a/tools/ld-export-metadata/main.cpp b/tools/ld-export-metadata/main.cpp index e1f2783d2..555f5879f 100644 --- a/tools/ld-export-metadata/main.cpp +++ b/tools/ld-export-metadata/main.cpp @@ -28,6 +28,7 @@ #include #include "csv.h" +#include "ffmetadata.h" #include "lddecodemetadata.h" @@ -123,6 +124,11 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "file")); parser.addOption(writeVbiCsvOption); + QCommandLineOption writeFfmetadataOption("ffmetadata", + QCoreApplication::translate("main", "Write navigation information as FFMETADATA1"), + QCoreApplication::translate("main", "file")); + parser.addOption(writeFfmetadataOption); + // -- Positional arguments -- // Positional argument to specify input video file @@ -167,6 +173,13 @@ int main(int argc, char *argv[]) return 1; } } + if (parser.isSet(writeFfmetadataOption)) { + const QString &fileName = parser.value(writeFfmetadataOption); + if (!writeFfmetadata(metaData, fileName)) { + qCritical() << "Failed to write output file:" << fileName; + return 1; + } + } // Quit with success return 0;