• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 35742451820

22 Sep 2026 02:44PM UTC coverage: 40.337% (+0.3%) from 40.008%
35742451820

push

github

jcelerier
gfx: name DirectShow camera formats by codec, and widen the subtypes

A compressed pin carries no pixel format, so CameraSettings holds
AV_PIX_FMT_NONE for it and av_get_pix_fmt_name answers nullptr. The device
list's description built its name from that with only MJPEG special-cased,
which left every other compressed codec assigning a null pointer to a
std::string. Gfx::cameraFormatName names a format from its codec when it has
no layout and from its layout otherwise, and neither lookup is used
unchecked.

The compressed half of the subtype decision now goes to the fourcc -> codec
table FFmpeg maintains for RIFF/AVI, which is what ffmpeg's own dshow device
resolves a pin through, rather than the six-entry list it replaces. H.264's
eight spellings, DV's five, VP8/VP9/AV1, MPEG-4/DIVX/Xvid, H.263, JPEG 2000,
v210 and the lossless card codecs all arrive from there. The layout table is
consulted first, so a fourcc it names stays raw even where that table calls
it a codec -- Y41P, which we unpack.

What that table lacks and DirectShow emits is listed alongside it: H.265 in
both spellings in the wild (UVC cameras label the pin 'H265', OBS and the LAV
filters 'HEVC'), the CFCC/IJPG/TVMJ/WAKE/Plum Motion-JPEG fourccs from
dshow.h, and MDVF. A camera whose only pin is H.265 had no format to offer
at all.

The raw table gains the aliases FFmpeg's own raw table carries, so a pin
spelt any of them resolves to the layout we offer it under: HDYC (the UYVY
capture cards present), the other packed 4:2:2 spellings, the planar AVI
ones, YUV9, Y411 and 'Y16 '.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QnTb3Zd2ctRWoTs5wMtoiC

51 of 52 new or added lines in 3 files covered. (98.08%)

8408 existing lines in 137 files now uncovered.

119310 of 295781 relevant lines covered (40.34%)

112391.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.05
/src/plugins/score-lib-process/Process/ProjectFilesApplicationPlugin.cpp
1
#include <Process/MediaTrimDialog.hpp>
2
#include <Process/MissingFilesDialog.hpp>
3
#include <Process/ProjectArchive.hpp>
4
#include <Process/UnusedFilesDialog.hpp>
5
#include <Process/ProjectConsolidation.hpp>
6
#include <Process/ProjectConsolidationDialog.hpp>
7
#include <Process/ProjectFilesApplicationPlugin.hpp>
8

9
#include <score/actions/ActionManager.hpp>
10
#include <score/actions/MenuManager.hpp>
11
#include <score/serialization/JSONVisitor.hpp>
12
#include <score/tools/Zip.hpp>
13
#include <score/widgets/HelpInteraction.hpp>
14

15
#include <core/document/Document.hpp>
16
#include <core/presenter/DocumentManager.hpp>
17

18
#include <QAction>
19
#include <QCoreApplication>
20
#include <QFileDialog>
21
#include <QFileInfo>
22
#include <QLocale>
23
#include <QMainWindow>
24
#include <QMenu>
25
#include <QMessageBox>
26
#include <QProgressDialog>
27
#include <QTemporaryDir>
28
#include <QTimer>
29

30
SCORE_DECLARE_ACTION(
5,610✔
31
    ConsolidateProject, "&Consolidate project...", Common, QKeySequence::UnknownKey)
32
SCORE_DECLARE_ACTION(
5,610✔
33
    LocateMissingFiles, "&Locate missing files...", Common, QKeySequence::UnknownKey)
34
SCORE_DECLARE_ACTION(
5,610✔
35
    TrimProjectMedia, "&Shorten media files to what is played...", Common,
36
    QKeySequence::UnknownKey)
37
SCORE_DECLARE_ACTION(
5,610✔
38
    RemoveUnusedFiles, "&Remove unused files...", Common, QKeySequence::UnknownKey)
39
SCORE_DECLARE_ACTION(
5,610✔
40
    ArchiveProject, "&Archive project...", Common, QKeySequence::UnknownKey)
41

42
namespace Process
43
{
44

45
ProjectFilesApplicationPlugin::ProjectFilesApplicationPlugin(
1,688✔
46
    const score::GUIApplicationContext& ctx)
47
    : score::GUIApplicationPlugin{ctx}
1,688✔
48
{
1,688✔
UNCOV
49
}
×
50

51
ProjectFilesApplicationPlugin::~ProjectFilesApplicationPlugin() = default;
3,372✔
52

53
score::Document*
54
ProjectFilesApplicationPlugin::documentWithFolder(const QString& what)
×
55
{
56
  auto doc = context.docManager.currentDocument();
×
UNCOV
57
  if(!doc)
×
UNCOV
58
    return nullptr;
×
59

60
  // Every path this writes is relative to the document's own folder, so there
61
  // has to be one. This is the "save your set into its own project folder
62
  // first" rule every DAW ends up teaching its users; ask for it up front
63
  // rather than silently working in the wrong place.
64
  if(doc->metadata().projectFolder().isEmpty())
×
65
  {
66
    const auto res = QMessageBox::question(
×
UNCOV
67
        context.mainWindow, QObject::tr("Save the document first"),
×
68
        QObject::tr("%1 works next to the document, so the document has to be "
×
69
                    "saved somewhere first.\n\nSave it now?")
70
            .arg(what),
×
71
        QMessageBox::Save | QMessageBox::Cancel);
×
72
    if(res != QMessageBox::Save)
×
73
      return nullptr;
×
74
    if(!context.docManager.saveDocumentAs(*doc))
×
75
      return nullptr;
×
76
    if(doc->metadata().projectFolder().isEmpty())
×
UNCOV
77
      return nullptr;
×
78
  }
×
79

UNCOV
80
  return doc;
×
81
}
×
82

83
void ProjectFilesApplicationPlugin::consolidate()
×
84
{
85
  auto doc = documentWithFolder(QObject::tr("Consolidating"));
×
UNCOV
86
  if(!doc)
×
87
    return;
×
88

89
  ProjectConsolidationDialog dialog{doc->context(), context.mainWindow};
×
UNCOV
90
  if(dialog.exec() != QDialog::Accepted)
×
UNCOV
91
    return;
×
92

93
  // Save right away: the copies on disk and the rewritten references only
94
  // make sense together, and a document closed without saving would leave
95
  // the collected files orphaned.
UNCOV
96
  context.docManager.saveDocument(*doc);
×
97
}
×
98

99
void ProjectFilesApplicationPlugin::locateMissingFiles()
×
100
{
101
  auto doc = context.docManager.currentDocument();
×
UNCOV
102
  if(!doc)
×
103
    return;
×
104

105
  auto* dialog = new MissingFilesDialog{*doc, context.mainWindow};
×
UNCOV
106
  dialog->show();
×
107
}
×
108

109
void ProjectFilesApplicationPlugin::trimMedia()
×
110
{
111
  auto doc = documentWithFolder(QObject::tr("Trimming"));
×
UNCOV
112
  if(!doc)
×
113
    return;
×
114

115
  MediaTrimDialog dialog{doc->context(), context.mainWindow};
×
UNCOV
116
  if(dialog.exec() != QDialog::Accepted)
×
117
    return;
×
118

UNCOV
119
  context.docManager.saveDocument(*doc);
×
120
}
×
121

122
void ProjectFilesApplicationPlugin::removeUnused()
×
123
{
124
  auto doc = documentWithFolder(QObject::tr("Cleaning up"));
×
UNCOV
125
  if(!doc)
×
126
    return;
×
127

128
  UnusedFilesDialog dialog{doc->context(), context.mainWindow};
×
UNCOV
129
  dialog.exec();
×
130
}
×
131

132
void ProjectFilesApplicationPlugin::archive()
×
133
{
134
#if defined(__EMSCRIPTEN__)
135
  // A web page has no folder to save next to and no path to write to: build the
136
  // archive in the in-memory filesystem and hand it to the browser as a
137
  // download. A document that was never saved still archives.
138
  auto doc = context.docManager.currentDocument();
139
  if(!doc)
140
    return;
141

142
  QString name = doc->metadata().documentName();
143
  if(name.isEmpty() || name.startsWith(QObject::tr("Untitled")))
144
    name = QStringLiteral("untitled");
145

146
  QTemporaryDir tmp;
147
  if(!tmp.isValid())
148
  {
149
    QMessageBox::warning(
150
        context.mainWindow, QObject::tr("Archiving failed"),
151
        QObject::tr("Could not create a working folder."));
152
    return;
153
  }
154

155
  // The score always goes in: it may be all this document has.
156
  const QString scorePath = tmp.filePath(name + QStringLiteral(".score"));
157
  {
158
    JSONReader w;
159
    w.buffer.Reserve(1024 * 1024 * 16);
160
    doc->saveAsJson(w);
161
    QFile f{scorePath};
162
    if(!f.open(QIODevice::WriteOnly))
163
    {
164
      QMessageBox::warning(
165
          context.mainWindow, QObject::tr("Archiving failed"),
166
          QObject::tr("Could not write the score."));
167
      return;
168
    }
169
    const qint64 sz = qint64(w.buffer.GetSize());
170
    if(f.write(w.buffer.GetString(), sz) != sz)
171
    {
172
      QMessageBox::warning(
173
          context.mainWindow, QObject::tr("Archiving failed"),
174
          QObject::tr("Could not write the score."));
175
      return;
176
    }
177
  }
178

179
  std::vector<score::ZipEntry> contents{
180
      {scorePath, name + QStringLiteral("/") + name + QStringLiteral(".score")}};
181

182
  // Media the document actually uses, when there is a folder to find them in.
183
  if(!doc->metadata().projectFolder().isEmpty())
184
  {
185
    const auto report = consolidateProjectFiles(doc->context(), {});
186
    for(auto& e : projectArchiveContents(doc->context(), report))
187
      if(!e.nameInArchive.endsWith(QStringLiteral(".score")))
188
        contents.push_back(e);
189
  }
190

191
  const QString zipPath = tmp.filePath(name + QStringLiteral(".zip"));
192
  QString error;
193
  if(!score::writeZipArchive(zipPath, contents, 1, error))
194
  {
195
    QMessageBox::warning(context.mainWindow, QObject::tr("Archiving failed"), error);
196
    return;
197
  }
198

199
  QFile zf{zipPath};
200
  if(!zf.open(QIODevice::ReadOnly))
201
  {
202
    QMessageBox::warning(
203
        context.mainWindow, QObject::tr("Archiving failed"),
204
        QObject::tr("Could not read the archive back."));
205
    return;
206
  }
207
  QFileDialog::saveFileContent(
208
      zf.readAll(), name + QStringLiteral(".zip"), context.mainWindow);
209
#else
UNCOV
210
  auto doc = documentWithFolder(QObject::tr("Archiving"));
×
UNCOV
211
  if(!doc)
×
UNCOV
212
    return;
×
213

214
  const QString suggested
UNCOV
215
      = doc->metadata().projectFolder() + '/' + doc->metadata().documentName()
×
UNCOV
216
        + QStringLiteral(".zip");
×
UNCOV
217
  const QString destination = QFileDialog::getSaveFileName(
×
UNCOV
218
      context.mainWindow, QObject::tr("Archive project as"), suggested,
×
UNCOV
219
      QObject::tr("Archives (*.zip)"));
×
UNCOV
220
  if(destination.isEmpty())
×
UNCOV
221
    return;
×
222

223
  // Collect first: an archive of a project whose media is scattered over the
224
  // machine would be an archive of nothing.
225
  const auto report = consolidateProjectFiles(doc->context(), {});
×
226
  context.docManager.saveDocument(*doc);
227

228
  const auto contents = projectArchiveContents(doc->context(), report);
229
  if(contents.empty())
×
230
  {
231
    QMessageBox::warning(
×
232
        context.mainWindow, QObject::tr("Nothing to archive"),
×
UNCOV
233
        QObject::tr("No file of this project could be found on disk."));
×
UNCOV
234
    return;
×
235
  }
236

237
  QProgressDialog progress{
238
      QObject::tr("Archiving %1 file(s), %2...")
239
          .arg(contents.size())
240
          .arg(QLocale{}.formattedDataSize(archiveContentsSize(contents))),
241
      QObject::tr("Cancel"), 0, int(contents.size()), context.mainWindow};
242
  progress.setWindowModality(Qt::WindowModal);
243

244
  QString error;
245
  const bool ok = score::writeZipArchive(
UNCOV
246
      destination, contents, /*level=*/1, error, [&](int done, int total) {
×
UNCOV
247
    progress.setValue(done);
×
UNCOV
248
    QCoreApplication::processEvents();
×
UNCOV
249
    return !progress.wasCanceled();
×
250
      });
251

252
  progress.close();
253

254
  if(!ok)
×
255
  {
UNCOV
256
    QMessageBox::warning(context.mainWindow, QObject::tr("Archiving failed"), error);
×
UNCOV
257
    return;
×
258
  }
259

260
  const int missing = report.count(FileAction::Missing);
261
  const int external = report.count(FileAction::Unsupported);
262
  QString note = QObject::tr("%1 holds %2 file(s).")
263
                     .arg(QFileInfo{destination}.fileName())
264
                     .arg(contents.size());
UNCOV
265
  if(missing > 0)
×
UNCOV
266
    note += QObject::tr("\n\n%1 file(s) could not be found and are NOT in the "
×
267
                        "archive.")
UNCOV
268
                .arg(missing);
×
UNCOV
269
  if(external > 0)
×
UNCOV
270
    note += QObject::tr("\n\n%1 external dependency/ies (plug-ins, folders) must "
×
271
                        "be installed on the other machine.")
UNCOV
272
                .arg(external);
×
273

274
  QMessageBox::information(context.mainWindow, QObject::tr("Project archived"), note);
275
#endif
UNCOV
276
}
×
277

278
void ProjectFilesApplicationPlugin::on_loadedDocument(score::Document& doc)
24✔
279
{
280
  if(!context.mainWindow)
24✔
281
    return;
5✔
282

283
  // The document is still being built: look at it once the load has settled,
284
  // and never block it.
285
  QPointer<score::Document> guard{&doc};
19✔
286
  QTimer::singleShot(0, this, [this, guard] {
38✔
287
    if(!guard)
19✔
UNCOV
288
      return;
×
289
    if(MissingFilesDialog::nothingMissing(guard->context()))
19✔
290
      return;
18✔
291

292
    auto* dialog = new MissingFilesDialog{*guard, context.mainWindow};
1✔
293
    dialog->show();
1✔
294
  });
19✔
295
}
24✔
296

297
void ProjectFilesApplicationPlugin::on_documentSaveAs(
34✔
298
    score::Document& doc, const QString& newFileName)
299
{
300
  const QString before = doc.metadata().projectFolder();
34✔
301
  if(before.isEmpty())
34✔
302
    return; // never saved: nothing was relative to anything yet
34✔
303

UNCOV
304
  const QString after = QFileInfo{newFileName}.absolutePath();
×
UNCOV
305
  if(before == after || QFileInfo{after}.canonicalFilePath() == before)
×
UNCOV
306
    return; // same folder: the relative references still mean what they meant
×
307

UNCOV
308
  const auto& ctx = doc.context();
×
UNCOV
309
  const int relative = countProjectRelativeFiles(ctx);
×
UNCOV
310
  if(relative == 0)
×
UNCOV
311
    return;
×
312

313
  // Saving elsewhere silently turns every "<PROJECT>:..." into a file that is
314
  // not there. Neither outcome is obviously right, so ask; but never leave the
315
  // document in the broken third state.
316
  // Without a window to ask in (scripted saves), re-anchor: copying gigabytes
317
  // of media is not something to do behind a script's back, and the document
318
  // still works afterwards.
UNCOV
319
  auto choice = QMessageBox::No;
×
UNCOV
320
  if(context.mainWindow)
×
321
  {
UNCOV
322
    choice = QMessageBox::StandardButton(QMessageBox::question(
×
UNCOV
323
        context.mainWindow, QObject::tr("Media of this project"),
×
UNCOV
324
        QObject::tr("This project is being saved to a different folder, and %1 of "
×
325
                    "its file(s) live next to the previous one.\n\n"
326
                    "Copy them into the new folder?\n\n"
327
                    "Choosing No keeps them where they are and points the project "
328
                    "at them with absolute paths.")
UNCOV
329
            .arg(relative),
×
UNCOV
330
        QMessageBox::Yes | QMessageBox::No));
×
UNCOV
331
  }
×
332

UNCOV
333
  if(choice == QMessageBox::Yes)
×
UNCOV
334
    consolidateProjectFiles(ctx, {}, after);
×
335
  else
336
    reanchorProjectFiles(ctx);
337
}
34✔
338

339
score::GUIElements ProjectFilesApplicationPlugin::makeGUIElements()
1,627✔
340
{
341
  score::GUIElements e;
1,627✔
342
  if(!context.mainWindow)
1,627✔
343
    return e;
505✔
344

345
  // Five entries is too many to leave loose in the File menu, and two of them
346
  // -- shortening files and removing files -- read as near-synonyms until they
347
  // are seen side by side. Their own submenu does both jobs.
348
  auto* project = new QMenu{QObject::tr("Project &files")};
1,122✔
349
  e.menus.emplace_back(project, score::Menus::ProjectFiles());
1,122✔
350

351
  auto& file = context.menus.get().at(score::Menus::File());
1,122✔
352
  auto& cond = context.actions.condition<score::EnableActionIfDocument>();
1,122✔
353

354
  {
355
    // A plug-in's additions land at the end of the menu, which here is after
356
    // Quit. Go in above the last separator instead, beside Save As.
357
    QAction* before = nullptr;
1,122✔
358
    const auto existing = file.menu()->actions();
1,122✔
359
    for(int i = existing.size() - 1; i >= 0; --i)
3,366✔
360
    {
361
      if(existing[i]->isSeparator())
3,366✔
362
      {
363
        before = existing[i];
1,122✔
364
        break;
1,122✔
365
      }
366
    }
2,244✔
367

368
    if(before)
1,122✔
369
      file.menu()->insertMenu(before, project);
1,122✔
370
    else
UNCOV
371
      file.menu()->addMenu(project);
×
372
  }
1,122✔
373

374
  const auto add = [&](QAction*& action, auto&& slot, const QString& help) {
6,732✔
375
    action = new QAction{context.mainWindow};
5,610✔
376
    score::setHelp(action, help);
5,610✔
377
    connect(action, &QAction::triggered, this, slot);
5,610✔
378
    project->addAction(action);
5,610✔
379
  };
5,610✔
380

381
  add(m_consolidate, &ProjectFilesApplicationPlugin::consolidate,
1,122✔
382
      QObject::tr("Copy every file the document uses next to it, and make the "
1,122✔
383
                  "references relative to the project."));
384
  e.actions.add<Actions::ConsolidateProject>(m_consolidate);
1,122✔
385
  cond.add<Actions::ConsolidateProject>();
1,122✔
386

387
  add(m_locate, &ProjectFilesApplicationPlugin::locateMissingFiles,
1,122✔
388
      QObject::tr("List the files this document cannot find and help point it at "
1,122✔
389
                  "them."));
390
  e.actions.add<Actions::LocateMissingFiles>(m_locate);
1,122✔
391
  cond.add<Actions::LocateMissingFiles>();
1,122✔
392

393
  project->addSeparator();
1,122✔
394

395
  add(m_unused, &ProjectFilesApplicationPlugin::removeUnused,
1,122✔
396
      QObject::tr("List the files sitting in the project folder that nothing in "
1,122✔
397
                  "the document uses any more, and get rid of them."));
398
  e.actions.add<Actions::RemoveUnusedFiles>(m_unused);
1,122✔
399
  cond.add<Actions::RemoveUnusedFiles>();
1,122✔
400

401
  add(m_trim, &ProjectFilesApplicationPlugin::trimMedia,
1,122✔
402
      QObject::tr("Shorten media files that are used down to the parts that are "
1,122✔
403
                  "played. Whole files nothing uses are removed by the entry "
404
                  "above instead."));
405
  e.actions.add<Actions::TrimProjectMedia>(m_trim);
1,122✔
406
  cond.add<Actions::TrimProjectMedia>();
1,122✔
407

408
  project->addSeparator();
1,122✔
409

410
  add(m_archive, &ProjectFilesApplicationPlugin::archive,
1,122✔
411
      QObject::tr("Collect everything and write the whole project into a single "
1,122✔
412
                  "zip file."));
413
  e.actions.add<Actions::ArchiveProject>(m_archive);
1,122✔
414
  cond.add<Actions::ArchiveProject>();
1,122✔
415

416
  return e;
1,122✔
417
}
1,627✔
418
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc