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;