• 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

90.11
/src/plugins/score-lib-process/Process/Drop/ProcessDropHandler.cpp
1
#include <Process/Drop/ProcessDropHandler.hpp>
2

3
#include <score/tools/File.hpp>
4

5
#include <ossia/detail/algorithms.hpp>
6

7
#include <QDebug>
8
#include <QFile>
9
#include <QFileInfo>
10
#include <QSet>
11
#include <QUrl>
12
namespace Process
13
{
14

15
ProcessDropHandler::ProcessDropHandler() { }
35,461✔
16

17
ProcessDropHandler::~ProcessDropHandler() { }
35,419✔
18

19
void ProcessDropHandler::getCustomDrops(
43✔
20
    std::vector<ProcessDropHandler::ProcessDrop>& drops, const QMimeData& mime,
21
    const score::DocumentContext& ctx) const noexcept
22
{
23
  // dropCustom is no longer noexcept (some overrides invoke parsers that
24
  // can throw on malformed input — see ProcessDropHandler.hpp). Catch
25
  // here so a throwing handler never escapes through the noexcept
26
  // public API and tears down the editor.
27
  try
28
  {
29
    dropCustom(drops, mime, ctx);
43✔
30
  }
43✔
31
  catch(const std::exception& e)
32
  {
33
    qWarning() << "ProcessDropHandler::dropCustom threw:" << e.what();
1✔
34
  }
1✔
35
  catch(...)
36
  {
37
    qWarning() << "ProcessDropHandler::dropCustom threw an unknown exception";
1✔
38
  }
2✔
39
}
45✔
40

41
void ProcessDropHandler::getMimeDrops(
18✔
42
    std::vector<ProcessDropHandler::ProcessDrop>& drops, const QMimeData& mime,
43
    const QString& fmt, const score::DocumentContext& ctx) const noexcept
44
{
45
  qDebug() << fmt << mime.data(fmt);
18✔
46
  auto df = DroppedFile{{}, mime.data(fmt)};
18✔
47
  dropData(drops, df, ctx);
18✔
48
}
18✔
49

50
void ProcessDropHandler::getFileDrops(
21✔
51
    std::vector<ProcessDropHandler::ProcessDrop>& drops, const QMimeData& mime,
52
    const score::FilePath& path, const score::DocumentContext& ctx) const noexcept
53
{
54
  // Check for handling through paths
55
  auto old_sz = drops.size();
21✔
56
  dropPath(drops, path, ctx);
21✔
57
  if(drops.size() != old_sz)
21✔
58
    return;
11✔
59

60
  // Fall back to manual handling
61
  if(QFile file{path.absolute}; file.open(QIODevice::ReadOnly))
20✔
62
  {
63
    dropData(drops, {path, file.readAll()}, ctx);
10✔
64
  }
10✔
65
}
21✔
66

67
QSet<QString> ProcessDropHandler::mimeTypes() const noexcept
22,178✔
68
{
69
  return {};
22,178✔
70
}
71

72
QSet<QString> ProcessDropHandler::fileExtensions() const noexcept
3,413✔
73
{
74
  return {};
3,413✔
75
}
76

77
void ProcessDropHandler::dropCustom(
13✔
78
    std::vector<ProcessDropHandler::ProcessDrop>&, const QMimeData& data,
79
    const score::DocumentContext& ctx) const
80
{
81
}
13✔
82

83
void ProcessDropHandler::dropPath(
×
84
    std::vector<ProcessDropHandler::ProcessDrop>&, const score::FilePath& data,
85
    const score::DocumentContext& ctx) const noexcept
86
{
87
}
×
88

89
void ProcessDropHandler::dropData(
28✔
90
    std::vector<ProcessDropHandler::ProcessDrop>&, const DroppedFile& data,
91
    const score::DocumentContext& ctx) const noexcept
92
{
93
}
28✔
94

95
ProcessDropHandlerList::~ProcessDropHandlerList() { }
3,372✔
96

97
std::vector<ProcessDropHandler::ProcessDrop> ProcessDropHandlerList::getDrop(
18✔
98
    const QMimeData& originalMime, const score::DocumentContext& ctx) const noexcept
99
{
100
  std::vector<ProcessDropHandler::ProcessDrop> res;
18✔
101

102
  initCaches();
18✔
103

104
#if defined(__EMSCRIPTEN__)
105
  // On wasm, Qt writes dropped files to a transient /qt/tmp/ dir and deletes
106
  // them immediately after this callback returns. Move each dropped file into
107
  // score's persistent import area up front and rewrite the URLs, so BOTH the
108
  // custom-drop path (which re-reads mime.urls() itself, e.g. Sound) and the
109
  // file-extension path see a path that survives the drop and can be re-opened
110
  // later (undo/redo, reload of the process).
111
  QMimeData stagedMime;
112
  bool didStage = false;
113
  {
114
    QList<QUrl> newUrls;
115
    for(const QUrl& url : originalMime.urls())
116
    {
117
      // On the JSPI build (which we use), Qt hands dropped files a
118
      // "weblocalfile:/N/name" URL backed by the JS File object; QUrl::toLocalFile()
119
      // is empty but QFile can read it through QWasmFileEngine. On non-asyncify
120
      // builds files land at /qt/tmp instead. In both cases read the bytes here
121
      // (while the source is alive) and copy them into a real, persistent MEMFS
122
      // path so the fopen-based decoders (ffmpeg / sndfile) can open them later.
123
      QString src;
124
      if(const QString local = url.toLocalFile();
125
         !local.isEmpty() && QFileInfo::exists(local))
126
        src = local;
127
      else if(url.scheme() == QLatin1String("weblocalfile"))
128
        src = url.toString();
129

130
      if(!src.isEmpty())
131
      {
132
        if(QFile f{src}; f.open(QIODevice::ReadOnly))
133
        {
134
          const QByteArray bytes = f.readAll();
135
          f.close();
136
          QString name = url.fileName();
137
          if(name.isEmpty())
138
            name = QFileInfo{src}.fileName();
139
          if(QString staged = score::stageImportedFile(name, bytes); !staged.isEmpty())
140
          {
141
            newUrls.push_back(QUrl::fromLocalFile(staged));
142
            didStage = true;
143
            continue;
144
          }
145
        }
146
      }
147
      newUrls.push_back(url);
148
    }
149
    if(didStage)
150
    {
151
      for(const QString& fmt : originalMime.formats())
152
        stagedMime.setData(fmt, originalMime.data(fmt));
153
      stagedMime.setUrls(newUrls);
154
    }
155
  }
156
  const QMimeData& mime = didStage ? stagedMime : originalMime;
157
#else
158
  const QMimeData& mime = originalMime;
18✔
159
#endif
160

161
  auto handleCustomDrop = [&](Process::ProcessDropHandler& handler) {
58✔
162
    auto before = res.size();
40✔
163
    handler.getCustomDrops(res, mime, ctx);
40✔
164
    auto after = res.size();
40✔
165
    return after != before;
40✔
166
  };
167

168
  // Look for drop handlers with the available MIME types
169
  for(const auto& fmt : mime.formats())
36✔
170
  {
171
    if(auto it = m_perMimeTypes.find(fmt.toStdString()); it != m_perMimeTypes.end())
18✔
172
    {
173
      auto& handler = *it->second;
18✔
174

175
      // First check if a custom drop is in order, which handles everything.
176
      if(handleCustomDrop(handler))
18✔
177
        return res;
×
178

179
      // Then fall back to the normal mime data drop
180
      handler.getMimeDrops(res, mime, fmt, ctx);
18✔
181
    }
18✔
182
  }
183

184
  if(!res.empty())
18✔
185
  {
186
    // qDebug() << "handled through getMimeDrops";
187
    return res;
×
188
  }
189

190
  // Look for drop handlers with the available file extensions
191
  for(const auto& url : mime.urls())
37✔
192
  {
193
    auto path = url.toLocalFile();
19✔
194

195
    QFileInfo f{path};
19✔
196
    if(f.exists())
19✔
197
    {
198
      auto ext = f.suffix().toLower();
15✔
199
      if(auto it = m_perFileExtension.find(ext.toStdString());
15✔
200
         it != m_perFileExtension.end())
15✔
201
      {
202
        const score::FilePath p{
42✔
203
            .absolute = path,
14✔
204
            .relative = score::relativizeFilePath(path, ctx),
14✔
205
            .filename = f.fileName(),
14✔
206
            .basename = f.baseName()};
14✔
207

208
        for(auto* h : it->second)
35✔
209
        {
210
          auto& handler = *h;
22✔
211

212
          // First check if a custom drop is in order, which handles everything.
213
          if(handleCustomDrop(handler))
22✔
214
          {
215
            // qDebug() << "handled through getCustomDrops";
216
            return res;
1✔
217
          }
218

219
          // Then fall back to the normal mime data drop
220
          handler.getFileDrops(res, mime, p, ctx);
21✔
221
        }
222
      }
14✔
223
    }
15✔
224
  }
19✔
225

226
  // TODO Fix Sound::DropHandler::drop so that we don't need to do that
227
  // and remove the custom drop mechanism above (problem is that customData is empty)
228
  /*
229
  {
230
    auto comp = [](auto& lhs, auto& rhs) {
231
      return lhs.creation.customData < rhs.creation.customData;
232
    };
233
    ossia::remove_duplicates(res, comp);
234
  }
235
  if(!res.empty())
236
  {
237
    qDebug() << "handled through getFileDrops";
238
    return res;
239
  }
240
  */
241
  return res;
17✔
242
}
18✔
243

244
std::optional<TimeVal> ProcessDropHandlerList::getMaxDuration(
×
245
    const std::vector<ProcessDropHandler::ProcessDrop>& res)
246
{
247
  using drop_t = Process::ProcessDropHandler::ProcessDrop;
248
  SCORE_ASSERT(!res.empty());
×
249

250
  auto max_t
251
      = std::max_element(res.begin(), res.end(), [](const drop_t& l, const drop_t& r) {
×
UNCOV
252
          return l.duration < r.duration;
×
253
        });
254

UNCOV
255
  return max_t->duration;
×
256
}
257

258
void ProcessDropHandlerList::initCaches() const
18✔
259
{
260
  if(m_lastCacheSize != this->size())
18✔
261
  {
262
    m_perMimeTypes.clear();
18✔
263
    m_perFileExtension.clear();
18✔
264

265
    for(auto& handler : *this)
396✔
266
    {
267
      for(const auto& ext : handler.fileExtensions())
2,124✔
268
      {
269
        m_perFileExtension[ext.toLower().toStdString()].push_back(&handler);
1,746✔
270
      }
271

272
      for(const auto& ext : handler.mimeTypes())
486✔
273
      {
274
        m_perMimeTypes[ext.toLower().toStdString()] = &handler;
108✔
275
      }
276
    }
277
  }
18✔
278
}
18✔
279
}
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