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

ossia / score / 33466651246

01 Sep 2026 03:33AM UTC coverage: 23.689% (+0.1%) from 23.563%
33466651246

push

github

jcelerier
tests: a latched-out exit reports that closing may proceed

RegressionDoubleExitTest asserted that a second Presenter::exit() returns false,
and its header documented that as the contract: "any further call while exiting
(or after a completed exit) is a no-op returning false".

That return is also what View::closeEvent turns into accept/ignore:

    if(m_presenter->exit()) ev->accept(); else ev->ignore();

and since Qt 6.6 QCoreApplication::quit() closes the top-level windows and
honours the refusal. So false made the latch veto the very quit that
forceExit() had just scheduled, and no windowed instance could be shut down over
OSC /exit -- the defect fixed in fe3fd08a64, which changed exit() to answer true
and left this test asserting the behaviour it had just removed. Master has been
red since that merge; I opened it without running this test.

The property the test is named for is unchanged and still asserted: a second
request is a no-op that does not re-enter closeAllDocuments. Only the answer it
gives the caller changes, from "refuse the close" to "closing may proceed".

Negative-controlled: with `== false` restored, 2 of 4 assertions fail; with
`== true`, 4/4 pass.

53523 of 225936 relevant lines covered (23.69%)

62475.92 hits per line

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

54.1
/src/plugins/score-plugin-media/Video/VideoDecoder.cpp
1
#include "VideoDecoder.hpp"
2

3
#include <Media/Libav.hpp>
4
#include <Video/GpuFormats.hpp>
5

6
#include <score/tools/Debug.hpp>
7

8
#include <ossia/detail/flicks.hpp>
9
#include <ossia/detail/libav.hpp>
10
#include <ossia/detail/thread.hpp>
11

12
#include <QApplication>
13
#include <QDebug>
14
#include <QElapsedTimer>
15
#include <QTimer>
16

17
#include <functional>
18
#include <iostream>
19
#include <thread>
20

21
#if SCORE_HAS_LIBAV
22

23
extern "C" {
24
#include <libavcodec/avcodec.h>
25
#include <libavformat/avformat.h>
26
#include <libavutil/pixdesc.h>
27
#include <libswscale/swscale.h>
28
#include <libavcodec/packet.h>
29
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 3, 100)
30
#if __has_include(<libavutil/mastering_display_metadata.h>)
31
#include <libavutil/mastering_display_metadata.h>
32
#endif
33
#endif
34
}
35

36
#if __APPLE__ && __has_include(<libavcodec/videotoolbox.h>)
37
#include "VideoDecoder.vtb.cpp"
38
#endif
39

40
namespace Video
41
{
42
#if LIBAVUTIL_VERSION_MAJOR >= 57
43
static auto get_format_for_codeccontext(AVCodecContext* ctx, const AVPixelFormat* p)
×
44
{
45
  //qDebug() << "device: " << av_pix_fmt_desc_get(ctx->pix_fmt)->name;
46

47
  if(auto self = (LibAVDecoder*)ctx->opaque)
×
48
  {
49
    while(*p != AV_PIX_FMT_NONE)
×
50
    {
51
      //qDebug() << av_pix_fmt_desc_get(*p)->name;
52
      // Check if the format matches the one we want from the expected HWDec
53
      if(*p == self->m_conf.hardwareAcceleration)
×
54
      {
55
        // Check if the format is indeed available
56
        auto fmt = ffmpegHardwareDecodingFormats(*p).format;
×
57
        if(fmt != AV_PIX_FMT_NONE)
×
58
        {
59
          return fmt;
×
60
        }
61
      }
×
62
      ++p;
×
63
    }
64
  }
×
65

66
  return ctx->pix_fmt;
×
67
}
×
68
#endif
69

70
void LibAVDecoder::init_scaler(VideoInterface& self) noexcept
1✔
71
{
72
  if(!Video::formatNeedsDecoding(self.pixel_format))
1✔
73
    return;
1✔
74

75
  m_rescale.open(self);
×
76

77
  // Only claim RGBA once the rescaler that produces it actually opened:
78
  // sws_getContext refuses some source descriptions (a stream whose
79
  // codecpar->format is AV_PIX_FMT_NONE, for one), and the renderer copies
80
  // color_space out of this metadata and applies it to frames that are still
81
  // in their native YUV.
82
  if(!m_rescale)
×
83
    return;
×
84

85
  self.pixel_format = AV_PIX_FMT_RGBA;
×
86
  self.color_space = AVCOL_SPC_RGB;
×
87
}
1✔
88

89
int LibAVDecoder::init_codec_context(
5✔
90
    const AVCodec* codec, AVBufferRef* hw_dev_ctx, const AVStream* stream,
91
    std::function<void(AVCodecContext&)> setup)
92
{
93
  m_codecContext = avcodec_alloc_context3(codec);
5✔
94

95
  avcodec_parameters_to_context(m_codecContext, stream->codecpar);
5✔
96

97
  // m_codecContext->flags |= AV_CODEC_FLAG_LOW_DELAY;
98
  // m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;
99
#if LIBAVUTIL_VERSION_MAJOR >= 57
100
  if(hw_dev_ctx)
5✔
101
  {
102
    m_codecContext->hw_device_ctx = hw_dev_ctx;
×
103
    m_codecContext->opaque = (void*)this;
×
104
    m_codecContext->get_format = get_format_for_codeccontext;
×
105
    m_codecContext->thread_count = 1;
×
106
    m_codecContext->thread_type = FF_THREAD_SLICE;
×
107
  }
×
108
  else
109
#endif
110
  {
111
#if defined(__EMSCRIPTEN__)
112
    // Force single-threaded video decoding on wasm. With the default (threads=0,
113
    // i.e. ffmpeg auto = CPU-count frame threads), avcodec_open2 sets up a
114
    // multithreaded decoder whose teardown (avcodec_flush_buffers /
115
    // avcodec_free_context) crashes on the emscripten pthread runtime -- even
116
    // when no frame was ever decoded.
117
    m_codecContext->thread_count = 1;
118
    m_codecContext->thread_type = 0;
119
#else
120
    m_codecContext->thread_count = m_conf.threads;
5✔
121
    if(m_conf.threads > 0)
5✔
122
      m_codecContext->thread_type = FF_THREAD_SLICE;
3✔
123
#endif
124
  }
125

126
  SCORE_ASSERT(setup);
5✔
127
  setup(*m_codecContext);
5✔
128

129
  int err = avcodec_open2(m_codecContext, codec, nullptr);
5✔
130
  if(err < 0)
5✔
131
  {
132
    qDebug() << "avcodec_open2: " << av_to_string(err);
×
133
    avcodec_free_context(&m_codecContext);
×
134
  }
×
135
  return err;
5✔
136
}
×
137

138
bool LibAVDecoder::open_codec_context(
1✔
139
    VideoInterface& self, const AVStream* stream,
140
    std::function<void(AVCodecContext&)> setup)
141
{
142
  if(auto [hw_dev_ctx, hw_codec] = open_hwdec(*m_codec); hw_codec)
1✔
143
  {
144
    int err = init_codec_context(hw_codec, hw_dev_ctx, stream, setup);
×
145
    if(err == 0)
×
146
    {
147
      init_scaler(self);
×
148
      return true;
×
149
    }
150
  }
×
151

152
  // Maybe opening an HW accel failed, we retry in software mode
153
  int err = init_codec_context(m_codec, nullptr, stream, setup);
1✔
154
  if(err == 0)
1✔
155
  {
156
    init_scaler(self);
1✔
157
    return true;
1✔
158
  }
159
  return false;
×
160
}
1✔
161

162
/*
163
 *
164
    using codec_map_type = ossia::flat_map<AVCodecID, const char*>;
165
    static const codec_map_type codecs{
166
        {AV_CODEC_ID_AV1, "av1_cuvid"},          {AV_CODEC_ID_H264, "h264_cuvid"},
167
        {AV_CODEC_ID_HEVC, "hevc_cuvid"},        {AV_CODEC_ID_MJPEG, "mjpeg_cuvid"},
168
        {AV_CODEC_ID_MPEG1VIDEO, "mpeg1_cuvid"}, {AV_CODEC_ID_MPEG2VIDEO, "mpeg2_cuvid"},
169
        {AV_CODEC_ID_MPEG4, "mpeg4_cuvid"},      {AV_CODEC_ID_VC1, "vc1_cuvid"},
170
        {AV_CODEC_ID_VP8, "vp8_cuvid"},          {AV_CODEC_ID_VP9, "vp9_cuvid"},
171
    };
172
    */
173
std::pair<AVBufferRef*, const AVCodec*>
174
LibAVDecoder::open_hwdec(const AVCodec& detected_codec) noexcept
1✔
175
{
176
#if LIBAVUTIL_VERSION_MAJOR >= 57
177
  auto hwAccel = m_conf.hardwareAcceleration;
1✔
178
  if(hwAccel == AV_PIX_FMT_NONE)
1✔
179
    return {};
1✔
180

181
  if(hwAccel == AV_PIX_FMT_NONE
×
182
     || !codecSupportsHWPixelFormat(detected_codec.id, hwAccel))
×
183
  {
184
    auto autoFmt = selectHardwareAcceleration(
×
185
        m_conf.graphicsApi, detected_codec.id, m_conf.gpuVendorId);
×
186
    if(autoFmt != AV_PIX_FMT_NONE)
×
187
      hwAccel = autoFmt;
×
188
    else
189
      return {};
×
190
  }
×
191

192
  const auto device = ffmpegHardwareDecodingFormats(hwAccel).device;
×
193
  if(device == AV_HWDEVICE_TYPE_NONE)
×
194
    return {};
×
195

196
  auto mapped = Video::hwCodecName(detected_codec.name, device);
×
197
  if(mapped.empty())
×
198
    return {};
×
199

200
  auto codec = mapped == detected_codec.name
×
201
                   ? &detected_codec // VideoToolbox case
×
202
                   : avcodec_find_decoder_by_name(mapped.c_str());
×
203
  if(!codec)
×
204
    return {};
×
205

206
  if(hwAccel == AV_PIX_FMT_DRM_PRIME)
×
207
  {
208
    // V4L2M2M: just want to map h264 to h264_v4l2m2m,
209
    // this isn't a true "hwdevice" accel
210
    return {nullptr, codec};
×
211
  }
212

213
  AVBufferRef* hw_device_ctx{};
×
214
  int ret = av_hwdevice_ctx_create(&hw_device_ctx, device, nullptr, nullptr, 0);
×
215
  if(ret != 0)
×
216
    return {};
×
217

218
  if(hwAccel == AV_PIX_FMT_QSV)
×
219
    return {hw_device_ctx, codec};
×
220
  else
221
    return {hw_device_ctx, &detected_codec};
×
222
#else
223
  return {};
224
#endif
225
}
1✔
226

227
ReadFrame LibAVDecoder::enqueue_frame(const AVPacket* pkt) noexcept
160✔
228
{
229
  auto receive = [this]() -> ReadFrame
478✔
230
  {
231
    auto frame = m_frames.newFrame();
318✔
232
    auto read
233
        = receiveVideoFrame(m_codecContext, frame.get(), this->m_conf.ignorePTS);
318✔
234
    if(read.error == AVERROR_EOF)
318✔
235
      m_finished = true;
6✔
236

237
    if(!read.frame)
318✔
238
    {
239
      m_frames.enqueue_decoding_error(frame.release());
161✔
240
      return read;
161✔
241
    }
242

243
    if(m_rescale)
157✔
244
    {
245
      m_rescale.rescale(m_frames, frame, read);
×
246
    }
×
247
    else if(read.frame == frame.get())
157✔
248
    {
249
      frame.release();
157✔
250
    }
157✔
251

252
    return read;
157✔
253
  };
318✔
254

255
  ReadFrame last{nullptr, AVERROR(EAGAIN)};
160✔
256
  auto keepInOrder = [this, &last](ReadFrame read) {
317✔
257
    if(last.frame)
157✔
258
      m_frames.enqueue(last.frame);
18✔
259
    last = read;
157✔
260
  };
157✔
261

262
  // A decoder may refuse a new packet until every pending frame has been
263
  // received. Drain those frames and retry the exact same packet.
264
  for(;;)
160✔
265
  {
266
    const int ret = avcodec_send_packet(m_codecContext, pkt);
164✔
267
    if(ret == 0)
164✔
268
      break;
159✔
269
    if(ret != AVERROR(EAGAIN))
5✔
270
    {
271
      if(ret == AVERROR_EOF)
1✔
272
        m_finished = true;
1✔
273
      else
274
        qDebug() << "avcodec_send_packet: " << av_to_string(ret) << ret;
×
275
      return last.frame ? last : ReadFrame{nullptr, ret};
1✔
276
    }
277

278
    auto read = receive();
4✔
279
    if(read.frame)
4✔
280
    {
281
      keepInOrder(read);
2✔
282
      continue;
2✔
283
    }
284

285
    // error == 0 with no frame: a frame was decoded but discarded (negative
286
    // pts). The codec still made room, so the packet must be retried, not
287
    // dropped.
288
    if(read.error == 0)
2✔
289
      continue;
2✔
290

291
    // EAGAIN from send_packet guarantees receive_frame yields a frame; if it
292
    // does not, no progress is possible: bail out instead of spinning.
293
    return last.frame ? last : read;
×
294
  }
295

296
  // One packet can make more than one frame available. Receive all of them
297
  // before reading the next packet so inter-frame reference chains stay intact.
298
  for(;;)
159✔
299
  {
300
    auto read = receive();
314✔
301
    if(read.frame)
314✔
302
    {
303
      keepInOrder(read);
155✔
304
      continue;
155✔
305
    }
306
    // A decoded-but-discarded frame (negative pts) is not the end of the
307
    // available frames: keep draining.
308
    if(read.error == 0)
159✔
309
      continue;
×
310
    if(read.error != AVERROR(EAGAIN) && read.error != AVERROR_EOF)
159✔
311
      return last.frame ? last : read;
×
312
    break;
159✔
313
  }
314

315
  if(last.frame)
159✔
316
    last.error = 0;
139✔
317
  return last;
159✔
318
}
160✔
319

320
#if 0
321
static void listHardwareDecodeTextureFormats(AVFrame* frame)
322
{
323
#if LIBAVUTIL_VERSION_MAJOR >= 57
324
  AVPixelFormat* arr = {};
325
  av_hwframe_transfer_get_formats(
326
      frame->hw_frames_ctx,
327
      AVHWFrameTransferDirection::AV_HWFRAME_TRANSFER_DIRECTION_FROM, &arr, 0);
328
  for(auto p = arr; *p != AV_PIX_FMT_NONE; ++p)
329
  {
330
    auto desc = av_pix_fmt_desc_get(*p);
331
    if(desc)
332
      qDebug() << "supported format : " << desc->name;
333
  }
334
  av_free(arr);
335
#endif
336
}
337
#endif
338

339
// Mainly used for HAP which we do not want to decode through ffmpeg
340
void LibAVDecoder::load_packet_in_frame(const AVPacket& packet, AVFrame& frame)
×
341
{
342
  auto cp = m_avstream->codecpar;
×
343
  // TODO this is a hack, we store the FOURCC in the format...
344

345
  memcpy(&frame.format, &cp->codec_tag, 4);
×
346

347
  frame.buf[0] = av_buffer_ref(packet.buf);
×
348
  frame.width = cp->width;
×
349
  frame.height = cp->height;
×
350
  frame.format = (cp->codec_tag);
×
351
  frame.best_effort_timestamp = packet.pts;
×
352
  frame.data[0] = packet.data;
×
353
  frame.linesize[0] = packet.size;
×
354
  frame.pts = packet.pts;
×
355
  frame.pkt_dts = packet.dts;
×
356
#if(LIBAVUTIL_VERSION_MAJOR < 58)
357
  frame.pkt_duration = packet.duration;
358
#else
359
  frame.duration = packet.duration;
×
360
#endif
361
}
×
362

363
ReadFrame receiveVideoFrame(
318✔
364
    AVCodecContext* codecContext, AVFrame* frame, bool ignorePts)
365
{
366
  if(codecContext && frame)
318✔
367
  {
368
    int ret = avcodec_receive_frame(codecContext, frame);
318✔
369

370
    if(ret < 0)
318✔
371
    {
372
      return {nullptr, ret};
159✔
373
    }
374
    else
375
    {
376
      if(ignorePts || frame->pts >= 0)
159✔
377
      {
378
#if LIBAVUTIL_VERSION_MAJOR >= 57
379
        // Transfer HW frame to CPU
380
        if(formatIsHardwareDecoded(AVPixelFormat(frame->format)))
157✔
381
        {
382
          AVFrame* sw_frame = av_frame_alloc();
×
383
          sw_frame->format = AV_PIX_FMT_NONE;
×
384

385
          int hw_ret = av_hwframe_transfer_data(sw_frame, frame, 0);
×
386
          if(hw_ret >= 0)
×
387
          {
388
            sw_frame->pts = frame->pts;
×
389
            av_frame_unref(frame);
×
390
            av_frame_move_ref(frame, sw_frame);
×
391
          }
×
392
          av_frame_free(&sw_frame);
×
393
          if(hw_ret < 0)
×
394
            return {nullptr, hw_ret};
×
395
        }
×
396
#endif
397
        return {frame, ret};
157✔
398
      }
399
      else
400
      {
401
        return {nullptr, ret};
2✔
402
      }
403
    }
404
  }
405

406
  return {nullptr, AVERROR_UNKNOWN};
×
407
}
318✔
408

409
VideoInterface::~VideoInterface() { }
9✔
410

411
VideoDecoder::VideoDecoder(DecoderConfiguration conf) noexcept
14✔
412
{
7✔
413
  m_conf = std::move(conf);
7✔
414
}
415

416
VideoDecoder::~VideoDecoder() noexcept
7✔
417
{
×
418
  close_file();
7✔
419
}
7✔
420

421
bool VideoDecoder::open(const std::string& inputFile) noexcept
7✔
422
{
423
  close_file();
7✔
424

425
  m_inputFile = inputFile;
7✔
426
  this->filePath = inputFile;
7✔
427

428
  if(avformat_open_input(&m_formatContext, inputFile.c_str(), nullptr, nullptr) != 0)
7✔
429
  {
430
    close_file();
6✔
431
    return false;
6✔
432
  }
433

434
  if(avformat_find_stream_info(m_formatContext, nullptr) < 0)
1✔
435
  {
436
    close_file();
×
437
    return false;
×
438
  }
439

440
  if(!open_stream())
1✔
441
  {
442
    close_file();
×
443
    return false;
×
444
  }
445

446
  int64_t secs = m_formatContext->duration / AV_TIME_BASE;
1✔
447
  int64_t us = m_formatContext->duration % AV_TIME_BASE;
1✔
448

449
  m_duration = secs * ossia::flicks_per_second<int64_t>;
1✔
450
  m_duration += us * ossia::flicks_per_millisecond<int64_t> / 1000;
1✔
451

452
  return true;
1✔
453
}
7✔
454

455
bool VideoDecoder::load(const std::string& inputFile) noexcept
1✔
456
{
457
  if(!open(inputFile))
1✔
458
    return false;
×
459

460
  m_running.store(true, std::memory_order_release);
1✔
461
  // TODO use a thread pool
462
  m_thread = std::thread{[this] {
2✔
463
    ossia::set_thread_name("ossia video");
1✔
464
    this->buffer_thread();
1✔
465
  }};
1✔
466

467
  return true;
1✔
468
}
1✔
469

470
int64_t VideoDecoder::duration() const noexcept
×
471
{
472
  return m_duration;
×
473
}
474

475
void VideoDecoder::seek(int64_t flicks)
×
476
{
477
  m_seekTo = flicks;
×
478
  m_condVar.notify_one();
×
479
}
×
480

481
AVFrame* VideoDecoder::dequeue_frame() noexcept
310✔
482
{
483
  auto f = m_frames.discard_and_dequeue_one();
310✔
484
  if(f)
310✔
485
  {
486
    m_last_dequeued_dts = f->pkt_dts;
50✔
487
  }
50✔
488
  m_condVar.notify_one();
310✔
489
  return f;
310✔
490
}
491

492
void VideoDecoder::release_frame(AVFrame* frame) noexcept
50✔
493
{
494
  m_frames.release(frame);
50✔
495
}
50✔
496

497
void VideoDecoder::buffer_thread() noexcept
1✔
498
{
499
  while(m_running.load(std::memory_order_acquire))
43✔
500
  {
501
    if(int64_t seek = m_seekTo.exchange(-1); seek >= 0)
43✔
502
    {
503
      seek_impl(seek);
×
504
    }
×
505
    else
506
    {
507
      std::unique_lock lck{m_condMut};
43✔
508
      m_condVar.wait(lck, [&] {
184✔
509
        return (m_frames.size() < frames_to_buffer / 2 && !m_finished)
282✔
510
               || !m_running.load(std::memory_order_acquire) || (m_seekTo != -1);
141✔
511
      });
512
      if(!m_running.load(std::memory_order_acquire))
43✔
513
        return;
1✔
514

515
      if(int64_t seek = m_seekTo.exchange(-1); seek >= 0)
42✔
516
      {
517
        seek_impl(seek);
×
518
      }
×
519

520
      if(m_frames.size() < (frames_to_buffer / 2) && !m_finished)
42✔
521
      {
522
        if(auto f = read_frame_impl())
42✔
523
        {
524
          m_frames.enqueue(f);
41✔
525
        }
41✔
526
        std::this_thread::sleep_for(std::chrono::milliseconds(4));
42✔
527
      }
42✔
528
    }
43✔
529
  }
530
}
1✔
531

532
void VideoDecoder::close_file() noexcept
20✔
533
{
534
  // Stop the running status
535
  m_running.store(false, std::memory_order_release);
20✔
536
  m_condVar.notify_one();
20✔
537

538
  if(m_thread.joinable())
20✔
539
    m_thread.join();
1✔
540

541
  // Remove frames that were in flight
542
  m_frames.drain();
20✔
543

544
  // Clear the stream
545
  close_video();
20✔
546

547
  // Clear the fmt context
548
  if(m_formatContext)
20✔
549
  {
550
    avio_flush(m_formatContext->pb);
1✔
551
    avformat_flush(m_formatContext);
1✔
552
    // avformat_close_input() already frees the context and sets it to nullptr;
553
    // do NOT also call avformat_free_context() on it (double free).
554
    avformat_close_input(&m_formatContext);
1✔
555
    m_formatContext = nullptr;
1✔
556
  }
1✔
557
}
20✔
558

559
ReadFrame LibAVDecoder::read_one_frame_raw(AVPacket& packet)
×
560
{
561
  int res{};
×
562

563
  while((res = av_read_frame(m_formatContext, &packet)) >= 0)
×
564
  {
565
    if(packet.stream_index == m_avstream->index)
×
566
    {
567
      auto frame = m_frames.newFrame();
×
568
      if(frame->buf[0])
×
569
        av_buffer_unref(&frame->buf[0]);
×
570
      // Mainly for HAP: we feed the raw undecoded codec data directly to the GPU, see HAPDecoder
571
      load_packet_in_frame(packet, *frame);
×
572

573
      av_packet_unref(&packet);
×
574
      return {frame.release(), 0};
×
575
    }
×
576
    else
577
    {
578
      av_packet_unref(&packet);
×
579
    }
580
  }
581

582
  if(res != 0 && res != AVERROR_EOF)
×
583
  {
584
    // qDebug() << "Error while reading a frame: "
585
    //          << av_to_string(res);
586
  }
×
587
  else if(res == AVERROR_EOF)
×
588
  {
589
    m_finished = true;
×
590
  }
×
591
  av_packet_unref(&packet);
×
592
  return {nullptr, res};
×
593
}
×
594

595
ReadFrame LibAVDecoder::read_one_frame_avcodec(AVPacket& packet)
42✔
596
{
597
  ReadFrame ret_frame;
42✔
598
  int res{};
42✔
599

600
  int z = 0;
42✔
601
do_read_frame:
602
  av_packet_unref(&packet);
51✔
603
  while((res = av_read_frame(m_formatContext, &packet)) >= 0)
51✔
604
  {
605
    if(packet.stream_index == m_avstream->index)
50✔
606
    {
607
      SCORE_ASSERT(m_codecContext);
50✔
608

609
      //av_packet_rescale_ts(
610
      //     &packet, this->m_avstream->time_base, this->m_codecContext->pkt_timebase);
611

612
      ret_frame = enqueue_frame(&packet);
50✔
613
      if(ret_frame.error == AVERROR(EAGAIN))
50✔
614
      {
615
        if(z++ < 100)
9✔
616
          goto do_read_frame;
9✔
617
      }
×
618
      av_packet_unref(&packet);
41✔
619
      return ret_frame;
41✔
620
    }
621
    else
622
    {
623
      av_packet_unref(&packet);
×
624
    }
625
  }
626

627
  if(res != 0 && res != AVERROR_EOF)
1✔
628
  {
629
    // qDebug() << "Error while reading a frame: "
630
    //          << av_to_string(res);
631
  }
×
632
  else if(res == AVERROR_EOF)
1✔
633
  {
634
    // Flush codec to get remaining frames from the reorder buffer (B-frames)
635
    if(m_codecContext)
1✔
636
    {
637
      // enqueue_frame routes every drained frame through the rescaler, like
638
      // every other decode path: these are the last frames of the clip, and a
639
      // decoder whose pixel_format was relabelled RGBA by init_scaler must
640
      // not suddenly emit its native format for them.
641
      auto flushed = enqueue_frame(nullptr);
1✔
642
      if(flushed.frame)
1✔
643
        m_frames.enqueue(flushed.frame);
1✔
644
    }
1✔
645
    m_finished = true;
1✔
646
  }
1✔
647
  av_packet_unref(&packet);
1✔
648
  return {nullptr, res};
1✔
649
}
42✔
650

651
ReadFrame LibAVDecoder::read_one_frame(AVPacket& packet)
42✔
652
{
653
  if(m_conf.useAVCodec)
42✔
654
    return read_one_frame_avcodec(packet);
42✔
655
  else
656
    return read_one_frame_raw(packet);
×
657
}
42✔
658
/*
659
// https://stackoverflow.com/a/44468529/1495627
660
static
661
int seek_to_frame(AVFormatContext* format, AVStream* stream, int frameIndex)
662
{
663
  using namespace std;
664
  // Seek is done on packet dts
665
  int64_t target_dts_usecs = std::round(frameIndex * (double)stream->r_frame_rate.den / stream->r_frame_rate.num * AV_TIME_BASE);
666
  // Remove first dts: when non zero seek should be more accurate
667
  auto first_dts_usecs = std::round(stream->first_dts * (double)stream->time_base.num / stream->time_base.den * AV_TIME_BASE);
668
  target_dts_usecs += first_dts_usecs;
669
  return av_seek_frame(format, -1, target_dts_usecs, AVSEEK_FLAG_BACKWARD);
670
}
671
*/
672

673
static int64_t to_av_time_base(AVRational tb, int64_t dts)
×
674
{
675
  constexpr auto av_tb = AVRational{1, AV_TIME_BASE};
×
676
  return av_rescale_q(dts, tb, av_tb);
×
677
}
678

679
bool VideoDecoder::seek_impl(int64_t flicks) noexcept
×
680
{
681
  if(m_avstream->index >= int(m_formatContext->nb_streams))
×
682
    return false;
×
683

684
  // Seeking with stream == -1 means that it is done AV_TIME_BASE
685
  constexpr auto av_tb = AVRational{1, AV_TIME_BASE};
×
686
  constexpr auto av_dts_per_flicks
×
687
      = (av_tb.den / (av_tb.num * ossia::flicks_per_second<double>));
688

689
  const int64_t dts = flicks * av_dts_per_flicks;
×
690

691
  const auto codec_tb
692
      = m_codecContext ? m_codecContext->pkt_timebase : m_avstream->time_base;
×
693

694
  // Don't seek if we're less than 0.2 second close to the request
695
  // unit of the timestamps in seconds: stream->time_base.num / stream->time_base.den
696

697
  // qDebug() << "Codec pkt_timebase: " << m_codecContext->pkt_timebase.num
698
  //          << m_codecContext->pkt_timebase.den;
699
  // qDebug() << "Codec timebase: " << m_codecContext->time_base.num
700
  //          << m_codecContext->time_base.den;
701
  // qDebug() << "Stream timebase: " << stream->time_base.num << stream->time_base.den;
702
  // qDebug() << "AV timebase: " << av_tb.num << av_tb.den;
703
  const auto last_av_dts = to_av_time_base(codec_tb, m_last_dequeued_dts);
×
704
  const int64_t min_dts_delta = (0.2 * av_tb.den) / av_tb.num;
×
705
  // qDebug() << AV_TIME_BASE << min_dts_delta << dts << last_av_dts << dts - last_av_dts
706
  //          << (std::abs(dts - last_av_dts) <= min_dts_delta);
707
  if(last_av_dts > INT64_MIN && std::abs(dts - last_av_dts) <= min_dts_delta)
×
708
  {
709
    // Let's always ensure that we seek to zero when asked no matter what
710
    if(dts != 0)
×
711
    {
712
      return false;
×
713
    }
714
  }
×
715

716
  // TODO - maybe we should also store the "last dequeued dts" from the
717
  // decoder side - this way no need to seek if we are in the interval
718
  // const bool seek_forward = dts >= this->m_last_dequeued_dts;
719
  // #if LIBAVFORMAT_VERSION_MAJOR >= 59
720
  //   const int64_t start = 0;
721
  // #else
722
  //   const int64_t start = m_avstream->first_dts;
723
  // #endif
724

725
  if(!ossia::seek_to_flick(m_formatContext, m_codecContext, m_avstream, flicks))
×
726
  {
727
    qDebug() << "Failed to seek for time ";
×
728
    return false;
×
729
  }
730

731
  ReadFrame r;
×
732
  do
×
733
  {
734
    // First flush the buffer or smth
735
    do
×
736
    {
737
      if(r.frame)
×
738
      {
739
        SCORE_LIBAV_FRAME_DEALLOC_CHECK(r.frame);
×
740
        av_frame_free(&r.frame);
×
741
      }
×
742

743
      auto pkt = av_packet_alloc();
×
744
      r = read_one_frame(*pkt);
×
745
      av_packet_unref(pkt);
×
746
      av_packet_free(&pkt);
×
747
    } while(r.error == AVERROR(EAGAIN));
×
748

749
    if(r.error == AVERROR_EOF || !r.frame)
×
750
    {
751
      break;
×
752
    }
753

754
    /*
755
    // Rescale the packet's dts into AV_TIME_BASE
756
    auto max_dts = r.frame->pkt_dts + r.frame->duration;
757
    auto max_av_dts = to_av_time_base(codec_tb, max_dts);
758
    //av_rescale_q(max_dts, stream->time_base, tb);
759
    // we're starting to see correct frames, try to get close to the dts we want.
760
    while(max_av_dts < dts)
761
    {
762
      r = read_one_frame(AVFramePointer{r.frame}, pkt);
763
      if(r.error == AVERROR_EOF || !r.frame)
764
        break;
765
    }
766
    */
767
  } while(0);
×
768

769
  if(r.frame)
×
770
  {
771
    m_frames.set_discard_frame(r.frame);
×
772
    m_frames.enqueue(r.frame);
×
773
  }
×
774
  else
775
  {
776
    SCORE_LIBAV_FRAME_DEALLOC_CHECK(r.frame);
×
777
    av_frame_free(&r.frame);
×
778
  }
779

780
  m_finished = false;
×
781

782
  return true;
×
783
}
×
784

785
AVFrame* VideoDecoder::read_frame_impl() noexcept
42✔
786
{
787
  ReadFrame res;
42✔
788

789
  if(m_avstream)
42✔
790
  {
791
    auto packet = av_packet_alloc();
42✔
792

793
    do
42✔
794
    {
795
      av_packet_unref(packet);
42✔
796
      res = read_one_frame(*packet);
42✔
797

798
      if(res.error == AVERROR_EOF)
42✔
799
      {
800
        m_finished = true;
1✔
801
        av_packet_unref(packet);
1✔
802
        av_packet_free(&packet);
1✔
803
        return res.frame;
1✔
804
      }
805
    } while(res.error == AVERROR(EAGAIN));
41✔
806

807
    av_packet_unref(packet);
41✔
808
    av_packet_free(&packet);
41✔
809
  }
41✔
810
  return res.frame;
41✔
811
}
42✔
812

813
bool VideoDecoder::open_stream() noexcept
1✔
814
{
815
  bool res = false;
1✔
816

817
  if(!m_formatContext)
1✔
818
    return res;
×
819

820
  int stream = -1;
1✔
821

822
  for(unsigned int i = 0; i < m_formatContext->nb_streams; i++)
2✔
823
  {
824
    if(m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
1✔
825
    {
826
      if(stream == -1)
1✔
827
      {
828
        stream = i;
1✔
829
        continue;
1✔
830
      }
831
    }
×
832
    m_formatContext->streams[i]->discard = AVDISCARD_ALL;
×
833
  }
×
834

835
  if(stream != -1)
1✔
836
  {
837
    m_avstream = m_formatContext->streams[stream];
1✔
838
    const AVRational tb = m_avstream->time_base;
1✔
839
    dts_per_flicks = (tb.den / (tb.num * ossia::flicks_per_second<double>));
1✔
840
    flicks_per_dts = (tb.num * ossia::flicks_per_second<double>) / tb.den;
1✔
841

842
    auto codecPar = m_avstream->codecpar;
1✔
843
    if((m_codec = avcodec_find_decoder(codecPar->codec_id)))
1✔
844
    {
845
      if(codecPar->width <= 0 || codecPar->height <= 0)
1✔
846
      {
847
        qDebug() << "VideoDecoder: invalid video: width or height is 0";
×
848
        res = false;
×
849
      }
×
850
      else
851
      {
852
        color_range = codecPar->color_range;
1✔
853
        color_primaries = codecPar->color_primaries;
1✔
854
        color_trc = codecPar->color_trc;
1✔
855
        color_space = codecPar->color_space;
1✔
856
        chroma_location = codecPar->chroma_location;
1✔
857

858
        // Detect wide-gamut / HDR evidence from primaries and transfer function.
859
        // This is used to infer color_space when it is unspecified.
860
        const bool has_bt2020_evidence =
1✔
861
            color_primaries == AVCOL_PRI_BT2020
1✔
862
            || color_trc == AVCOL_TRC_SMPTE2084      // PQ (HDR10 / BT.2100)
1✔
863
            || color_trc == AVCOL_TRC_ARIB_STD_B67;   // HLG (BT.2100)
1✔
864

865
        // Display P3 content may use BT.709 matrix coefficients
866
        // but with wider primaries. Don't force it to BT.2020.
867
        const bool has_p3_evidence =
1✔
868
            color_primaries == AVCOL_PRI_SMPTE432     // Display P3 (D65)
1✔
869
            || color_primaries == AVCOL_PRI_SMPTE431; // DCI-P3
1✔
870

871
        if(color_space == AVCOL_SPC_UNSPECIFIED)
1✔
872
        {
873
          if(has_bt2020_evidence)
1✔
874
            color_space = AVCOL_SPC_BT2020_NCL;
×
875
          else if(has_p3_evidence)
1✔
876
            // P3 content typically uses BT.709 matrix coefficients.
877
            // colorMatrix() will detect the P3 primaries and route
878
            // through the Display P3 pipeline.
879
            color_space = AVCOL_SPC_BT709;
×
880
          else if(codecPar->height < 625)
1✔
881
            color_space = AVCOL_SPC_SMPTE170M;
1✔
882
          else if(codecPar->height < 720)
×
883
            color_space = AVCOL_SPC_BT470BG;
×
884
          else
885
            color_space = AVCOL_SPC_BT709;
×
886
        }
1✔
887
        if(color_range == AVCOL_RANGE_UNSPECIFIED)
1✔
888
          color_range = AVCOL_RANGE_MPEG;
1✔
889

890
        // HDR handling
891
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 3, 100)
892
        {
893
          const auto data = codecPar->coded_side_data;
894
          const auto n = codecPar->nb_coded_side_data;
895
          // Light data
896
          if(auto sd = av_packet_side_data_get(data, n, AV_PKT_DATA_CONTENT_LIGHT_LEVEL))
897
            if(sd->data)
898
              this->content_light = *reinterpret_cast<const AVContentLightMetadata*>(sd->data);
899

900
          // Mastering side data
901
          if (auto sd = av_packet_side_data_get(data, n, AV_PKT_DATA_MASTERING_DISPLAY_METADATA))
902
            if(sd->data)
903
              this->mastering_display = *(AVMasteringDisplayMetadata *)sd->data;
904
        }
905
#endif
906

907
        codec_id = codecPar->codec_id;
1✔
908

909
        // Check if this is a GPU-direct codec (HAP or DXV DXT1/DXT5)
910
        bool use_gpu_direct = false;
1✔
911
        if(m_avstream->codecpar->codec_id == AV_CODEC_ID_HAP)
1✔
912
        {
913
          // HAP: store the FOURCC in the format for GPU decoder matching
914
          memcpy(&pixel_format, &m_avstream->codecpar->codec_tag, 4);
×
915
          use_gpu_direct = true;
×
916
        }
×
917
        else if(m_avstream->codecpar->codec_id == AV_CODEC_ID_DXV)
1✔
918
        {
919
          // DXV: peek first packet to determine sub-format (DXT1/DXT5)
920
          // Store synthetic fourcc in pixel_format for GPU decoder matching
921
          auto packet = av_packet_alloc();
×
922
          if(av_read_frame(m_formatContext, packet) >= 0 && packet->size >= 4)
×
923
          {
924
            uint32_t tag = packet->data[0] | (packet->data[1] << 8)
×
925
                           | (packet->data[2] << 16)
×
926
                           | ((uint32_t)packet->data[3] << 24);
×
927
            switch(tag)
×
928
            {
929
              case 0x44585431: // MKBETAG('D','X','T','1')
930
                memcpy(&pixel_format, "Dxv1", 4);
×
931
                use_gpu_direct = true;
×
932
                break;
×
933
              case 0x44585435: // MKBETAG('D','X','T','5')
934
                memcpy(&pixel_format, "Dxv5", 4);
×
935
                use_gpu_direct = true;
×
936
                break;
×
937
              case 0x59434736: // MKBETAG('Y','C','G','6')
938
                memcpy(&pixel_format, "DxvY", 4);
×
939
                use_gpu_direct = true;
×
940
                break;
×
941
              case 0x59473130: // MKBETAG('Y','G','1','0')
942
                memcpy(&pixel_format, "DxvA", 4);
×
943
                use_gpu_direct = true;
×
944
                break;
×
945
              default: {
946
                // Old format: check type flags in high byte
947
                uint8_t old_type = tag >> 24;
×
948
                if(old_type & 0x40)
×
949
                {
950
                  memcpy(&pixel_format, "Dxv5", 4);
×
951
                  use_gpu_direct = true;
×
952
                }
×
953
                else if(old_type & 0x20)
×
954
                {
955
                  memcpy(&pixel_format, "Dxv1", 4);
×
956
                  use_gpu_direct = true;
×
957
                }
×
958
                // Unknown old format falls through to avcodec
959
                break;
×
960
              }
961
            }
962
            av_packet_unref(packet);
×
963
          }
×
964
          av_packet_free(&packet);
×
965
          // Seek back to beginning regardless
966
          av_seek_frame(m_formatContext, m_avstream->index, 0, AVSEEK_FLAG_BACKWARD);
×
967
        }
×
968

969
        if(use_gpu_direct)
1✔
970
        {
971
          width = codecPar->width;
×
972
          height = codecPar->height;
×
973
          fps = av_q2d(m_avstream->avg_frame_rate);
×
974

975
          m_conf.useAVCodec = false;
×
976
          m_codecContext = nullptr;
×
977
          m_codec = nullptr;
×
978
          res = true;
×
979
        }
×
980
        else
981
        {
982
          pixel_format = (AVPixelFormat)codecPar->format;
1✔
983
          width = codecPar->width;
1✔
984
          height = codecPar->height;
1✔
985
          fps = av_q2d(m_avstream->avg_frame_rate);
1✔
986

987
          res = open_codec_context(*this, m_avstream, [this](AVCodecContext& ctx) {
2✔
988
            ctx.framerate
1✔
989
                = av_guess_frame_rate(m_formatContext, (AVStream*)m_avstream, NULL);
2✔
990
            m_codecContext->pkt_timebase = m_avstream->time_base;
1✔
991
            // m_codecContext->codec_id = m_codec->id;
992
          });
1✔
993

994
          if(m_codecContext)
1✔
995
          {
996
            auto tb = m_codecContext->pkt_timebase;
1✔
997
            dts_per_flicks = (tb.den / (tb.num * ossia::flicks_per_second<double>));
1✔
998
            flicks_per_dts = (tb.num * ossia::flicks_per_second<double>) / tb.den;
1✔
999
          }
1✔
1000
        }
1001
      }
1002
    }
1✔
1003
  }
1✔
1004

1005
  if(!res)
1✔
1006
  {
1007
    close_video();
×
1008
  }
×
1009
  return res;
1✔
1010
}
1✔
1011

1012
void VideoDecoder::close_video() noexcept
20✔
1013
{
1014
  if(m_codecContext)
20✔
1015
  {
1016
    avcodec_flush_buffers(m_codecContext);
1✔
1017
#if defined(__APPLE__)
1018
#if FF_API_VT_HWACCEL_CONTEXT
1019
    if(m_codecContext->hwaccel_context)
1020
      av_videotoolbox_default_free(m_codecContext);
1021
#endif
1022
#endif
1023
    avcodec_free_context(&m_codecContext);
1✔
1024

1025
    m_codecContext = nullptr;
1✔
1026
    m_codec = nullptr;
1✔
1027
  }
1✔
1028

1029
  m_rescale.close();
20✔
1030

1031
  m_avstream = nullptr;
20✔
1032
}
20✔
1033
}
1034
#endif
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