10#include <QDialogButtonBox>
14#include <QGuiApplication>
18#include <QJsonDocument>
24#include <QPlainTextEdit>
31#include <QTreeWidgetItem>
38 case QJsonValue::Bool:
39 return v.toBool() ? QStringLiteral(
"true") : QStringLiteral(
"false");
40 case QJsonValue::Double: {
41 const double d = v.toDouble();
42 if (d ==
static_cast<qint64
>(d))
43 return QString::number(
static_cast<qint64
>(d));
44 return QString::number(d,
'g', 10);
46 case QJsonValue::String:
48 case QJsonValue::Null:
49 return QStringLiteral(
"null");
50 case QJsonValue::Array:
51 case QJsonValue::Object:
52 return QString::fromUtf8(QJsonDocument(v.isArray() ? QJsonDocument(v.toArray()) : QJsonDocument(v.toObject())).toJson(QJsonDocument::Compact));
62 return QString::number(bps / 1'000'000.0,
'f', 2) +
" Mb/s";
64 return QString::number(bps / 1'000.0,
'f', 1) +
" kb/s";
65 return QString::number(bps) +
" b/s";
71 constexpr double KiB = 1024.0;
72 constexpr double MiB = KiB * 1024.0;
73 constexpr double GiB = MiB * 1024.0;
75 return QString::number(bytes / GiB,
'f', 2) +
" GiB";
77 return QString::number(bytes / MiB,
'f', 2) +
" MiB";
79 return QString::number(bytes / KiB,
'f', 1) +
" KiB";
80 return QString::number(bytes) +
" B";
95 QList<StreamRow> rows;
96 rows.reserve(streams.size());
97 for (
int i = 0; i < streams.size(); ++i) {
98 const QJsonObject s = streams.at(i).toObject();
101 r.
type = s.value(
"codec_type").toString();
102 r.
codec = s.value(
"codec_name").toString();
103 r.
profile = s.value(
"profile").toString();
104 if (r.
type ==
"video") {
105 r.
fmtPix = s.value(
"pix_fmt").toString();
106 const int w = s.value(
"width").toInt();
107 const int h = s.value(
"height").toInt();
108 const QString fps = s.value(
"avg_frame_rate").toString();
109 r.
sizeRate = QString(
"%1x%2 @ %3").arg(w).arg(h).arg(fps);
111 const QString cs = s.value(
"color_space").toString();
112 const QString cp = s.value(
"color_primaries").toString();
113 const QString tr = s.value(
"color_transfer").toString();
120 r.
color = tags.join(
'/');
121 }
else if (r.
type ==
"audio") {
122 r.
fmtPix = s.value(
"sample_fmt").toString();
123 const int sr = s.value(
"sample_rate").toString().toInt();
124 const int ch = s.value(
"channels").toInt();
125 r.
sizeRate = QString(
"%1 Hz, %2 ch").arg(sr).arg(ch);
127 const qint64 sBrate = s.value(
"bit_rate").toString().toLongLong();
143 if (frames.isEmpty())
145 const QJsonArray sideData = frames.first().toObject().value(
"side_data_list").toArray();
146 for (
const QJsonValue &sv : sideData) {
147 const QJsonObject so = sv.toObject();
148 const QString type = so.value(
"side_data_type").toString();
149 if (type ==
"Mastering display metadata") {
151 info.
redX = so.value(
"red_x").toString();
152 info.
redY = so.value(
"red_y").toString();
153 info.
greenX = so.value(
"green_x").toString();
154 info.
greenY = so.value(
"green_y").toString();
155 info.
blueX = so.value(
"blue_x").toString();
156 info.
blueY = so.value(
"blue_y").toString();
157 info.
whiteX = so.value(
"white_point_x").toString();
158 info.
whiteY = so.value(
"white_point_y").toString();
159 info.
maxLum = so.value(
"max_luminance").toString();
160 info.
minLum = so.value(
"min_luminance").toString();
161 }
else if (type ==
"Content light level metadata") {
163 info.
maxCll = so.value(
"max_content").toVariant().toString();
164 info.
maxFall = so.value(
"max_average").toVariant().toString();
173 setWindowTitle(tr(
"Media Metadata Viewer"));
177 auto *root =
new QVBoxLayout(
this);
180 auto *fileRow =
new QHBoxLayout();
182 pathEdit->setPlaceholderText(tr(
"Path to a media file (mp4, mkv, mov, ...)"));
188 root->addLayout(fileRow);
191 tabs =
new QTabWidget(
this);
193 tree =
new QTreeWidget(
this);
194 tree->setColumnCount(2);
195 tree->setHeaderLabels({tr(
"Field"), tr(
"Value")});
196 tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
197 tree->header()->setSectionResizeMode(1, QHeaderView::Stretch);
205 htmlPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
209 textPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
213 QFont monoFont(
"Courier New");
214 monoFont.setStyleHint(QFont::Monospace);
215 monoFont.setPointSize(10);
220 root->addWidget(
tabs, 1);
223 auto *actionRow =
new QHBoxLayout();
227 actionRow->addStretch(1);
230 root->addLayout(actionRow);
235 connect(
closeButton, &QPushButton::clicked,
this, &QDialog::accept);
243 const int idx =
tabs ?
tabs->currentIndex() : -1;
246 label = tr(
"Copy Markdown to Clipboard");
248 label = tr(
"Copy HTML to Clipboard");
250 label = tr(
"Copy Text to Clipboard");
252 label = tr(
"Copy to Clipboard");
257 const QString start =
pathEdit->text().isEmpty() ? QString() : QFileInfo(
pathEdit->text()).absolutePath();
258 const QString chosen = QFileDialog::getOpenFileName(
this, tr(
"Select Media File"), start, tr(
"Media files (*.mp4 *.mkv *.mov *.webm *.avi *.m4v *.ts *.flv);;All files (*.*)"));
259 if (!chosen.isEmpty()) {
265 const QString path =
pathEdit->text().trimmed();
266 if (path.isEmpty() || !QFileInfo::exists(path)) {
267 QMessageBox::warning(
this, tr(
"Metadata Viewer"), tr(
"Please select an existing media file."));
274 args <<
"-v" <<
"error"
275 <<
"-print_format" <<
"json"
279 <<
"-read_intervals" <<
"%+#1"
280 <<
"-show_entries" <<
"frame=side_data_list:format:stream" << path;
281 proc.start(
"ffprobe", args);
282 if (!proc.waitForStarted(5000)) {
283 QMessageBox::critical(
this, tr(
"Metadata Viewer"), tr(
"Failed to launch ffprobe. Is it installed and on PATH?"));
286 if (!proc.waitForFinished(30000)) {
288 QMessageBox::critical(
this, tr(
"Metadata Viewer"), tr(
"ffprobe timed out."));
291 if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) {
292 const QString err = QString::fromUtf8(proc.readAllStandardError());
293 QMessageBox::critical(
this, tr(
"Metadata Viewer"), tr(
"ffprobe failed:\n%1").arg(err.isEmpty() ? tr(
"(no error output)") : err));
297 QJsonParseError parseErr;
298 const QByteArray out = proc.readAllStandardOutput();
299 const QJsonDocument doc = QJsonDocument::fromJson(out, &parseErr);
300 if (parseErr.error != QJsonParseError::NoError || !doc.isObject()) {
301 QMessageBox::critical(
this, tr(
"Metadata Viewer"), tr(
"Failed to parse ffprobe JSON: %1").arg(parseErr.errorString()));
305 const QJsonObject root = doc.object();
314 auto *item =
new QTreeWidgetItem(parent);
315 item->setText(0, key);
316 if (value.isObject()) {
317 const QJsonObject obj = value.toObject();
318 item->setText(1, tr(
"{%1 fields}").arg(obj.size()));
319 for (
auto it = obj.begin(); it != obj.end(); ++it) {
322 }
else if (value.isArray()) {
323 const QJsonArray arr = value.toArray();
324 item->setText(1, tr(
"[%1 items]").arg(arr.size()));
325 for (
int i = 0; i < arr.size(); ++i) {
329 item->setText(1, jsonValueToString(value));
336 auto addRoot = [&](
const QString &label) -> QTreeWidgetItem * {
337 auto *item =
new QTreeWidgetItem(
tree);
338 item->setText(0, label);
339 item->setFirstColumnSpanned(
false);
343 if (root.contains(
"format")) {
344 auto *fmt = addRoot(tr(
"format"));
345 const QJsonObject obj = root.value(
"format").toObject();
346 for (
auto it = obj.begin(); it != obj.end(); ++it) {
349 fmt->setExpanded(
true);
351 if (root.contains(
"streams")) {
352 const QJsonArray arr = root.value(
"streams").toArray();
353 for (
int i = 0; i < arr.size(); ++i) {
354 const QJsonObject sObj = arr.at(i).toObject();
355 const QString codec = sObj.value(
"codec_type").toString();
356 auto *node = addRoot(tr(
"stream[%1] (%2)").arg(i).arg(codec));
357 for (
auto it = sObj.begin(); it != sObj.end(); ++it) {
360 node->setExpanded(i == 0);
363 if (root.contains(
"frames")) {
364 const QJsonArray arr = root.value(
"frames").toArray();
365 if (!arr.isEmpty()) {
366 auto *node = addRoot(tr(
"frame side data"));
367 for (
int i = 0; i < arr.size(); ++i) {
370 node->setExpanded(
true);
377 QTextStream out(&md);
379 const QJsonObject fmt = root.value(
"format").toObject();
380 const QString filename = fmt.value(
"filename").toString();
381 const QString fmtName = fmt.value(
"format_long_name").toString();
382 const double duration = fmt.value(
"duration").toString().toDouble();
383 const qint64 size = fmt.value(
"size").toString().toLongLong();
384 const qint64 brate = fmt.value(
"bit_rate").toString().toLongLong();
386 out <<
"## Media Metadata\n\n";
387 if (!filename.isEmpty())
388 out <<
"**File**: `" << QFileInfo(filename).fileName() <<
"` \n";
389 if (!fmtName.isEmpty())
390 out <<
"**Container**: " << fmtName <<
" \n";
392 out <<
"**Duration**: " << QString::number(duration,
'f', 3) <<
" s \n";
394 out <<
"**Size**: " << humanSize(size) <<
" \n";
396 out <<
"**Overall bitrate**: " << humanBitrate(brate) <<
" \n";
399 const QList<StreamRow> rows = collectStreams(root.value(
"streams").toArray());
400 if (!rows.isEmpty()) {
401 out <<
"### Streams\n\n";
402 out <<
"| # | Type | Codec | Profile | Pix/Sample fmt | Resolution / Rate | Color | Bitrate |\n";
403 out <<
"|---|------|-------|---------|----------------|-------------------|-------|---------|\n";
404 for (
const StreamRow &r : rows) {
405 out <<
"| " << r.index <<
" | " << r.type <<
" | " << r.codec <<
" | " << r.profile <<
" | " << r.fmtPix <<
" | " << r.sizeRate <<
" | " << r.color <<
" | " << r.bitrate <<
" |\n";
410 const HdrInfo hdr = collectHdr(root.value(
"frames").toArray());
411 if (hdr.hasMastering || hdr.hasCll) {
412 out <<
"### HDR Static Metadata\n\n";
413 if (hdr.hasMastering) {
414 out << QString(
"- **Mastering display**: R(%1, %2) G(%3, %4) B(%5, %6) "
415 "WP(%7, %8) L(%9, %10)\n")
416 .arg(hdr.redX, hdr.redY, hdr.greenX, hdr.greenY, hdr.blueX, hdr.blueY, hdr.whiteX, hdr.whiteY)
417 .arg(hdr.maxLum, hdr.minLum);
420 out <<
"- **MaxCLL**: " << hdr.maxCll <<
" cd/m^2\n";
421 out <<
"- **MaxFALL**: " << hdr.maxFall <<
" cd/m^2\n";
431 QTextStream out(&html);
433 const QJsonObject fmt = root.value(
"format").toObject();
434 const QString filename = fmt.value(
"filename").toString();
435 const QString fmtName = fmt.value(
"format_long_name").toString();
436 const double duration = fmt.value(
"duration").toString().toDouble();
437 const qint64 size = fmt.value(
"size").toString().toLongLong();
438 const qint64 brate = fmt.value(
"bit_rate").toString().toLongLong();
440 auto esc = [](
const QString &s) {
return s.toHtmlEscaped(); };
442 out <<
"<h2>Media Metadata</h2>\n";
444 if (!filename.isEmpty())
445 out <<
" <li><b>File:</b> <code>" << esc(QFileInfo(filename).fileName()) <<
"</code></li>\n";
446 if (!fmtName.isEmpty())
447 out <<
" <li><b>Container:</b> " << esc(fmtName) <<
"</li>\n";
449 out <<
" <li><b>Duration:</b> " << QString::number(duration,
'f', 3) <<
" s</li>\n";
451 out <<
" <li><b>Size:</b> " << esc(humanSize(size)) <<
"</li>\n";
453 out <<
" <li><b>Overall bitrate:</b> " << esc(humanBitrate(brate)) <<
"</li>\n";
456 const QList<StreamRow> rows = collectStreams(root.value(
"streams").toArray());
457 if (!rows.isEmpty()) {
458 out <<
"<h3>Streams</h3>\n";
459 out <<
"<table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">\n";
460 out <<
" <thead><tr>"
461 "<th>#</th><th>Type</th><th>Codec</th><th>Profile</th>"
462 "<th>Pix/Sample fmt</th><th>Resolution / Rate</th>"
463 "<th>Color</th><th>Bitrate</th></tr></thead>\n";
465 for (
const StreamRow &r : rows) {
467 <<
"<td>" << r.index <<
"</td>"
468 <<
"<td>" << esc(r.type) <<
"</td>"
469 <<
"<td>" << esc(r.codec) <<
"</td>"
470 <<
"<td>" << esc(r.profile) <<
"</td>"
471 <<
"<td>" << esc(r.fmtPix) <<
"</td>"
472 <<
"<td>" << esc(r.sizeRate) <<
"</td>"
473 <<
"<td>" << esc(r.color) <<
"</td>"
474 <<
"<td>" << esc(r.bitrate) <<
"</td>"
477 out <<
" </tbody>\n</table>\n";
480 const HdrInfo hdr = collectHdr(root.value(
"frames").toArray());
481 if (hdr.hasMastering || hdr.hasCll) {
482 out <<
"<h3>HDR Static Metadata</h3>\n<ul>\n";
483 if (hdr.hasMastering) {
484 out <<
" <li><b>Mastering display:</b> R(" << esc(hdr.redX) <<
", " << esc(hdr.redY) <<
") G(" << esc(hdr.greenX) <<
", " << esc(hdr.greenY) <<
") B(" << esc(hdr.blueX) <<
", " << esc(hdr.blueY) <<
") WP(" << esc(hdr.whiteX) <<
", " << esc(hdr.whiteY) <<
") L(" << esc(hdr.maxLum) <<
", " << esc(hdr.minLum) <<
")</li>\n";
487 out <<
" <li><b>MaxCLL:</b> " << esc(hdr.maxCll) <<
" cd/m²</li>\n";
488 out <<
" <li><b>MaxFALL:</b> " << esc(hdr.maxFall) <<
" cd/m²</li>\n";
498 QTextStream out(&txt);
500 const QJsonObject fmt = root.value(
"format").toObject();
501 const QString filename = fmt.value(
"filename").toString();
502 const QString fmtName = fmt.value(
"format_long_name").toString();
503 const double duration = fmt.value(
"duration").toString().toDouble();
504 const qint64 size = fmt.value(
"size").toString().toLongLong();
505 const qint64 brate = fmt.value(
"bit_rate").toString().toLongLong();
507 out <<
"Media Metadata\n";
508 out <<
"==============\n\n";
509 if (!filename.isEmpty())
510 out <<
"File : " << QFileInfo(filename).fileName() <<
"\n";
511 if (!fmtName.isEmpty())
512 out <<
"Container : " << fmtName <<
"\n";
514 out <<
"Duration : " << QString::number(duration,
'f', 3) <<
" s\n";
516 out <<
"Size : " << humanSize(size) <<
"\n";
518 out <<
"Overall bitrate: " << humanBitrate(brate) <<
"\n";
521 const QList<StreamRow> rows = collectStreams(root.value(
"streams").toArray());
522 if (!rows.isEmpty()) {
525 for (
const StreamRow &r : rows) {
526 out <<
"[" << r.index <<
"] " << r.type;
527 if (!r.codec.isEmpty())
528 out <<
" | codec=" << r.codec;
529 if (!r.profile.isEmpty())
530 out <<
" | profile=" << r.profile;
531 if (!r.fmtPix.isEmpty())
532 out <<
" | fmt=" << r.fmtPix;
533 if (!r.sizeRate.isEmpty())
534 out <<
" | " << r.sizeRate;
535 if (!r.color.isEmpty())
536 out <<
" | color=" << r.color;
537 if (!r.bitrate.isEmpty())
538 out <<
" | bitrate=" << r.bitrate;
544 const HdrInfo hdr = collectHdr(root.value(
"frames").toArray());
545 if (hdr.hasMastering || hdr.hasCll) {
546 out <<
"HDR Static Metadata\n";
547 out <<
"-------------------\n";
548 if (hdr.hasMastering) {
549 out <<
"Mastering display: "
550 <<
"R(" << hdr.redX <<
", " << hdr.redY <<
") "
551 <<
"G(" << hdr.greenX <<
", " << hdr.greenY <<
") "
552 <<
"B(" << hdr.blueX <<
", " << hdr.blueY <<
") "
553 <<
"WP(" << hdr.whiteX <<
", " << hdr.whiteY <<
") "
554 <<
"L(" << hdr.maxLum <<
", " << hdr.minLum <<
")\n";
557 out <<
"MaxCLL : " << hdr.maxCll <<
" cd/m^2\n";
558 out <<
"MaxFALL: " << hdr.maxFall <<
" cd/m^2\n";
567 const int idx =
tabs->currentIndex();
568 QPlainTextEdit *src =
nullptr;
572 format = tr(
"Markdown");
580 QMessageBox::information(
this, tr(
"Metadata Viewer"), tr(
"Switch to the Markdown, HTML, or Text tab to copy."));
584 const QString text = src ? src->toPlainText() : QString();
588 QClipboard *clip = QGuiApplication::clipboard();
593 auto *mime =
new QMimeData();
596 clip->setMimeData(mime, QClipboard::Clipboard);
598 if (clip->supportsSelection()) {
599 auto *selMime =
new QMimeData();
600 selMime->setText(text);
601 selMime->setHtml(text);
602 clip->setMimeData(selMime, QClipboard::Selection);
606 clip->setText(text, QClipboard::Clipboard);
608 if (clip->supportsSelection())
609 clip->setText(text, QClipboard::Selection);
613 QMessageBox::information(
this, tr(
"Metadata Viewer"), tr(
"%1 copied to clipboard.").arg(format));
void applyCustomStyleIfEnabled(QWidget *widget)