ACMX 2.139.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
metadata-viewer.cpp
Go to the documentation of this file.
1/**
2 * @file metadata-viewer.cpp
3 * @brief Implementation of the media metadata viewer dialog.
4 */
5
6#include "metadata-viewer.hpp"
7#include "custom_style.hpp"
8
9#include <QClipboard>
10#include <QDialogButtonBox>
11#include <QFileDialog>
12#include <QFileInfo>
13#include <QFont>
14#include <QGuiApplication>
15#include <QHBoxLayout>
16#include <QHeaderView>
17#include <QJsonArray>
18#include <QJsonDocument>
19#include <QJsonObject>
20#include <QJsonValue>
21#include <QLineEdit>
22#include <QMessageBox>
23#include <QMimeData>
24#include <QPlainTextEdit>
25#include <QProcess>
26#include <QPushButton>
27#include <QTabBar>
28#include <QTabWidget>
29#include <QTextStream>
30#include <QTreeWidget>
31#include <QTreeWidgetItem>
32#include <QVBoxLayout>
33
34namespace {
35
36 QString jsonValueToString(const QJsonValue &v) {
37 switch (v.type()) {
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);
45 }
46 case QJsonValue::String:
47 return v.toString();
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));
53 default:
54 return QString();
55 }
56 }
57
58 QString humanBitrate(qint64 bps) {
59 if (bps <= 0)
60 return QString();
61 if (bps >= 1'000'000)
62 return QString::number(bps / 1'000'000.0, 'f', 2) + " Mb/s";
63 if (bps >= 1'000)
64 return QString::number(bps / 1'000.0, 'f', 1) + " kb/s";
65 return QString::number(bps) + " b/s";
66 }
67
68 QString humanSize(qint64 bytes) {
69 if (bytes <= 0)
70 return QString();
71 constexpr double KiB = 1024.0;
72 constexpr double MiB = KiB * 1024.0;
73 constexpr double GiB = MiB * 1024.0;
74 if (bytes >= GiB)
75 return QString::number(bytes / GiB, 'f', 2) + " GiB";
76 if (bytes >= MiB)
77 return QString::number(bytes / MiB, 'f', 2) + " MiB";
78 if (bytes >= KiB)
79 return QString::number(bytes / KiB, 'f', 1) + " KiB";
80 return QString::number(bytes) + " B";
81 }
82
83 struct StreamRow {
84 int index = 0;
85 QString type;
86 QString codec;
87 QString profile;
88 QString fmtPix;
89 QString sizeRate;
90 QString color;
91 QString bitrate;
92 };
93
94 QList<StreamRow> collectStreams(const QJsonArray &streams) {
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();
99 StreamRow r;
100 r.index = i;
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);
110 QStringList tags;
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();
114 if (!cp.isEmpty())
115 tags << cp;
116 if (!tr.isEmpty())
117 tags << tr;
118 if (!cs.isEmpty())
119 tags << cs;
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);
126 }
127 const qint64 sBrate = s.value("bit_rate").toString().toLongLong();
128 r.bitrate = humanBitrate(sBrate);
129 rows.append(r);
130 }
131 return rows;
132 }
133
134 struct HdrInfo {
135 bool hasMastering = false;
137 bool hasCll = false;
138 QString maxCll, maxFall;
139 };
140
141 HdrInfo collectHdr(const QJsonArray &frames) {
142 HdrInfo info;
143 if (frames.isEmpty())
144 return info;
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") {
150 info.hasMastering = true;
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") {
162 info.hasCll = true;
163 info.maxCll = so.value("max_content").toVariant().toString();
164 info.maxFall = so.value("max_average").toVariant().toString();
165 }
166 }
167 return info;
168 }
169
170} // namespace
171
172MetadataViewer::MetadataViewer(QWidget *parent) : QDialog(parent) {
173 setWindowTitle(tr("Media Metadata Viewer"));
174 resize(900, 700);
176
177 auto *root = new QVBoxLayout(this);
178
179 // File selection row.
180 auto *fileRow = new QHBoxLayout();
181 pathEdit = new QLineEdit(this);
182 pathEdit->setPlaceholderText(tr("Path to a media file (mp4, mkv, mov, ...)"));
183 browseButton = new QPushButton(tr("Browse..."), this);
184 analyzeButton = new QPushButton(tr("Analyze"), this);
185 fileRow->addWidget(pathEdit, 1);
186 fileRow->addWidget(browseButton);
187 fileRow->addWidget(analyzeButton);
188 root->addLayout(fileRow);
189
190 // Tabbed display: parsed tree + Markdown/HTML/text previews.
191 tabs = new QTabWidget(this);
192
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);
198 tabs->addTab(tree, tr("Tree"));
199
200 markdownPreview = new QPlainTextEdit(this);
201 markdownPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
202 markdownTabIndex = tabs->addTab(markdownPreview, tr("Markdown"));
203
204 htmlPreview = new QPlainTextEdit(this);
205 htmlPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
206 htmlTabIndex = tabs->addTab(htmlPreview, tr("HTML"));
207
208 textPreview = new QPlainTextEdit(this);
209 textPreview->setLineWrapMode(QPlainTextEdit::NoWrap);
210 textTabIndex = tabs->addTab(textPreview, tr("Text"));
211
212 // Monospace font for previews.
213 QFont monoFont("Courier New");
214 monoFont.setStyleHint(QFont::Monospace);
215 monoFont.setPointSize(10);
216 markdownPreview->setFont(monoFont);
217 htmlPreview->setFont(monoFont);
218 textPreview->setFont(monoFont);
219
220 root->addWidget(tabs, 1);
221
222 // Action row.
223 auto *actionRow = new QHBoxLayout();
224 copyButton = new QPushButton(this);
225 closeButton = new QPushButton(tr("Close"), this);
226 copyButton->setEnabled(false);
227 actionRow->addStretch(1);
228 actionRow->addWidget(copyButton);
229 actionRow->addWidget(closeButton);
230 root->addLayout(actionRow);
231
232 connect(browseButton, &QPushButton::clicked, this, &MetadataViewer::browseFile);
233 connect(analyzeButton, &QPushButton::clicked, this, &MetadataViewer::analyzeFile);
234 connect(copyButton, &QPushButton::clicked, this, &MetadataViewer::copyCurrentTab);
235 connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
236 connect(pathEdit, &QLineEdit::returnPressed, this, &MetadataViewer::analyzeFile);
237 connect(tabs, &QTabWidget::currentChanged, this, [this](int) { updateCopyButtonLabel(); });
238
240}
241
243 const int idx = tabs ? tabs->currentIndex() : -1;
244 QString label;
245 if (idx == markdownTabIndex)
246 label = tr("Copy Markdown to Clipboard");
247 else if (idx == htmlTabIndex)
248 label = tr("Copy HTML to Clipboard");
249 else if (idx == textTabIndex)
250 label = tr("Copy Text to Clipboard");
251 else
252 label = tr("Copy to Clipboard");
253 copyButton->setText(label);
254}
255
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()) {
260 pathEdit->setText(chosen);
261 }
262}
263
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."));
268 return;
269 }
270
271 QProcess proc;
272 QStringList args;
273 // Pull format + streams + frame-level side data (HDR mastering, MaxCLL, etc).
274 args << "-v" << "error"
275 << "-print_format" << "json"
276 << "-show_format"
277 << "-show_streams"
278 << "-show_frames"
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?"));
284 return;
285 }
286 if (!proc.waitForFinished(30000)) {
287 proc.kill();
288 QMessageBox::critical(this, tr("Metadata Viewer"), tr("ffprobe timed out."));
289 return;
290 }
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));
294 return;
295 }
296
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()));
302 return;
303 }
304
305 const QJsonObject root = doc.object();
306 populateTree(root);
307 markdownPreview->setPlainText(buildMarkdown(root));
308 htmlPreview->setPlainText(buildHtml(root));
309 textPreview->setPlainText(buildPlainText(root));
310 copyButton->setEnabled(true);
311}
312
313void MetadataViewer::addJsonValue(QTreeWidgetItem *parent, const QString &key, const QJsonValue &value) {
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) {
320 addJsonValue(item, it.key(), it.value());
321 }
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) {
326 addJsonValue(item, QString::number(i), arr.at(i));
327 }
328 } else {
329 item->setText(1, jsonValueToString(value));
330 }
331}
332
333void MetadataViewer::populateTree(const QJsonObject &root) {
334 tree->clear();
335
336 auto addRoot = [&](const QString &label) -> QTreeWidgetItem * {
337 auto *item = new QTreeWidgetItem(tree);
338 item->setText(0, label);
339 item->setFirstColumnSpanned(false);
340 return item;
341 };
342
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) {
347 addJsonValue(fmt, it.key(), it.value());
348 }
349 fmt->setExpanded(true);
350 }
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) {
358 addJsonValue(node, it.key(), it.value());
359 }
360 node->setExpanded(i == 0);
361 }
362 }
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) {
368 addJsonValue(node, QString::number(i), arr.at(i));
369 }
370 node->setExpanded(true);
371 }
372 }
373}
374
375QString MetadataViewer::buildMarkdown(const QJsonObject &root) const {
376 QString md;
377 QTextStream out(&md);
378
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();
385
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";
391 if (duration > 0.0)
392 out << "**Duration**: " << QString::number(duration, 'f', 3) << " s \n";
393 if (size > 0)
394 out << "**Size**: " << humanSize(size) << " \n";
395 if (brate > 0)
396 out << "**Overall bitrate**: " << humanBitrate(brate) << " \n";
397 out << "\n";
398
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";
406 }
407 out << "\n";
408 }
409
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);
418 }
419 if (hdr.hasCll) {
420 out << "- **MaxCLL**: " << hdr.maxCll << " cd/m^2\n";
421 out << "- **MaxFALL**: " << hdr.maxFall << " cd/m^2\n";
422 }
423 out << "\n";
424 }
425
426 return md;
427}
428
429QString MetadataViewer::buildHtml(const QJsonObject &root) const {
430 QString html;
431 QTextStream out(&html);
432
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();
439
440 auto esc = [](const QString &s) { return s.toHtmlEscaped(); };
441
442 out << "<h2>Media Metadata</h2>\n";
443 out << "<ul>\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";
448 if (duration > 0.0)
449 out << " <li><b>Duration:</b> " << QString::number(duration, 'f', 3) << " s</li>\n";
450 if (size > 0)
451 out << " <li><b>Size:</b> " << esc(humanSize(size)) << "</li>\n";
452 if (brate > 0)
453 out << " <li><b>Overall bitrate:</b> " << esc(humanBitrate(brate)) << "</li>\n";
454 out << "</ul>\n";
455
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";
464 out << " <tbody>\n";
465 for (const StreamRow &r : rows) {
466 out << " <tr>"
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>"
475 << "</tr>\n";
476 }
477 out << " </tbody>\n</table>\n";
478 }
479
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";
485 }
486 if (hdr.hasCll) {
487 out << " <li><b>MaxCLL:</b> " << esc(hdr.maxCll) << " cd/m&sup2;</li>\n";
488 out << " <li><b>MaxFALL:</b> " << esc(hdr.maxFall) << " cd/m&sup2;</li>\n";
489 }
490 out << "</ul>\n";
491 }
492
493 return html;
494}
495
496QString MetadataViewer::buildPlainText(const QJsonObject &root) const {
497 QString txt;
498 QTextStream out(&txt);
499
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();
506
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";
513 if (duration > 0.0)
514 out << "Duration : " << QString::number(duration, 'f', 3) << " s\n";
515 if (size > 0)
516 out << "Size : " << humanSize(size) << "\n";
517 if (brate > 0)
518 out << "Overall bitrate: " << humanBitrate(brate) << "\n";
519 out << "\n";
520
521 const QList<StreamRow> rows = collectStreams(root.value("streams").toArray());
522 if (!rows.isEmpty()) {
523 out << "Streams\n";
524 out << "-------\n";
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;
539 out << "\n";
540 }
541 out << "\n";
542 }
543
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";
555 }
556 if (hdr.hasCll) {
557 out << "MaxCLL : " << hdr.maxCll << " cd/m^2\n";
558 out << "MaxFALL: " << hdr.maxFall << " cd/m^2\n";
559 }
560 out << "\n";
561 }
562
563 return txt;
564}
565
567 const int idx = tabs->currentIndex();
568 QPlainTextEdit *src = nullptr;
569 QString format;
570 if (idx == markdownTabIndex) {
571 src = markdownPreview;
572 format = tr("Markdown");
573 } else if (idx == htmlTabIndex) {
574 src = htmlPreview;
575 format = tr("HTML");
576 } else if (idx == textTabIndex) {
577 src = textPreview;
578 format = tr("text");
579 } else {
580 QMessageBox::information(this, tr("Metadata Viewer"), tr("Switch to the Markdown, HTML, or Text tab to copy."));
581 return;
582 }
583
584 const QString text = src ? src->toPlainText() : QString();
585 if (text.isEmpty())
586 return;
587
588 QClipboard *clip = QGuiApplication::clipboard();
589 if (idx == htmlTabIndex) {
590 // For HTML, also publish a rich-HTML representation so HTML-aware
591 // targets (mail composers, rich-text editors) get formatted output
592 // while plain-text targets get the raw HTML source.
593 auto *mime = new QMimeData();
594 mime->setText(text);
595 mime->setHtml(text);
596 clip->setMimeData(mime, QClipboard::Clipboard);
597#ifdef __linux__
598 if (clip->supportsSelection()) {
599 auto *selMime = new QMimeData();
600 selMime->setText(text);
601 selMime->setHtml(text);
602 clip->setMimeData(selMime, QClipboard::Selection);
603 }
604#endif
605 } else {
606 clip->setText(text, QClipboard::Clipboard);
607#ifdef __linux__
608 if (clip->supportsSelection())
609 clip->setText(text, QClipboard::Selection);
610#endif
611 }
612
613 QMessageBox::information(this, tr("Metadata Viewer"), tr("%1 copied to clipboard.").arg(format));
614}
void browseFile()
Open a file picker and store the chosen path in the path field.
QString buildMarkdown(const QJsonObject &root) const
Render a Markdown-formatted summary of the metadata.
void addJsonValue(QTreeWidgetItem *parent, const QString &key, const QJsonValue &value)
Recursively add a JSON value as a tree item.
QPlainTextEdit * textPreview
QString buildPlainText(const QJsonObject &root) const
Render a plain-text summary of the metadata.
MetadataViewer(QWidget *parent=nullptr)
QTabWidget * tabs
QPlainTextEdit * markdownPreview
QTreeWidget * tree
void updateCopyButtonLabel()
Refresh the copy button label based on the active tab.
QLineEdit * pathEdit
QString buildHtml(const QJsonObject &root) const
Render an HTML-formatted summary of the metadata.
QPlainTextEdit * htmlPreview
void copyCurrentTab()
Copy the currently selected tab's text (Markdown/HTML/Text) to the system clipboard.
QPushButton * analyzeButton
QPushButton * copyButton
QPushButton * closeButton
void populateTree(const QJsonObject &root)
Populate the tree widget from a parsed ffprobe JSON object.
QPushButton * browseButton
void analyzeFile()
Launch ffprobe against the current file and refresh the tree.
Media metadata viewer dialog.
void applyCustomStyleIfEnabled(QWidget *widget)
HdrInfo collectHdr(const QJsonArray &frames)
QList< StreamRow > collectStreams(const QJsonArray &streams)
QString jsonValueToString(const QJsonValue &v)