X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=video_widget.cpp;h=1f3534f117cdf425c6fc1ba048706f82580f4921;hb=f23692525864984376d5f97c3f6fcc72c2870861;hp=7bafa64e4b0a00f444eecbb40b2ef09d776cf98f;hpb=da70a59f7ed8e678117a463c28e6bea06e05431f;p=pkanalytics diff --git a/video_widget.cpp b/video_widget.cpp index 7bafa64..1f3534f 100644 --- a/video_widget.cpp +++ b/video_widget.cpp @@ -35,6 +35,10 @@ extern "C" { #include #include #include +#include +#include + +#define BUFFER_OFFSET(i) ((char *)nullptr + (i)) using namespace std; using namespace std::chrono; @@ -194,11 +198,7 @@ bool VideoWidget::process_queued_commands(AVFormatContext *format_ctx, AVCodecCo queue.pop_front(); queued_frames = std::move(queue); } - Frame *new_frame = new Frame(make_video_frame(frame.get())); - { - lock_guard lock(current_frame_mu); - current_frame.reset(new_frame); - } + video_window->set_current_frame(make_video_frame(frame.get())); update(); store_pts(frame->pts); break; @@ -229,11 +229,7 @@ bool VideoWidget::process_queued_commands(AVFormatContext *format_ctx, AVCodecCo if (frame == nullptr || error) { return true; } - Frame *new_frame = new Frame(make_video_frame(frame.get())); - { - lock_guard lock(current_frame_mu); - current_frame.reset(new_frame); - } + video_window->set_current_frame(make_video_frame(frame.get())); update(); store_pts(frame->pts); } @@ -241,7 +237,28 @@ bool VideoWidget::process_queued_commands(AVFormatContext *format_ctx, AVCodecCo } VideoWidget::VideoWidget(QWidget *parent) - : QOpenGLWidget(parent) {} + : QWidget(parent), + video_window(new VideoWindow(this)) { + setLayout(new QHBoxLayout); + layout()->setContentsMargins(QMargins()); + layout()->addWidget(QWidget::createWindowContainer(video_window)); + + // ... + connect(video_window, &VideoWindow::mouse_wheel, this, &VideoWidget::wheelEvent); + connect(video_window, &VideoWindow::mouse_pressed, this, &VideoWidget::mousePressEvent); + connect(video_window, &VideoWindow::mouse_released, this, &VideoWidget::mouseReleaseEvent); + connect(video_window, &VideoWindow::mouse_moved, this, &VideoWidget::mouseMoveEvent); +} + +VideoWidget::~VideoWidget() +{ + stop(); + + // Qt will delete video_window for us after we're gone, + // so make sure its destructor does not try to mess with + // our freelist. The actual freelist frames will leak. + video_window->set_current_frame(nullptr); +} GLuint compile_shader(const string &shader_src, GLenum type) { @@ -281,7 +298,7 @@ GLuint compile_shader(const string &shader_src, GLenum type) return obj; } -void VideoWidget::initializeGL() +void VideoWindow::initializeGL() { glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); @@ -364,7 +381,7 @@ void main() glSamplerParameteri(bilinear_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } -void VideoWidget::resizeGL(int w, int h) +void VideoWindow::resizeGL(int w, int h) { glViewport(0, 0, w, h); display_aspect = double(w) / h; @@ -381,9 +398,9 @@ int num_levels(GLuint width, GLuint height) return levels; } -void VideoWidget::paintGL() +void VideoWindow::paintGL() { - std::shared_ptr frame; + std::shared_ptr frame; { lock_guard lock(current_frame_mu); frame = current_frame; @@ -403,15 +420,25 @@ void VideoWidget::paintGL() } } - glTextureSubImage2D(tex[0], 0, 0, 0, frame->width, frame->height, GL_RED, GL_UNSIGNED_BYTE, frame->data.get()); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, frame->pbo); + + if (frame->need_flush_len > 0) { + glFlushMappedNamedBufferRange(frame->pbo, 0, frame->need_flush_len); + frame->need_flush_len = 0; + } + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glTextureSubImage2D(tex[0], 0, 0, 0, frame->width, frame->height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(0)); glGenerateTextureMipmap(tex[0]); - glTextureSubImage2D(tex[1], 0, 0, 0, frame->chroma_width, frame->chroma_height, GL_RED, GL_UNSIGNED_BYTE, frame->data.get() + frame->width * frame->height); + glTextureSubImage2D(tex[1], 0, 0, 0, frame->chroma_width, frame->chroma_height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(frame->width * frame->height)); glGenerateTextureMipmap(tex[1]); - glTextureSubImage2D(tex[2], 0, 0, 0, frame->chroma_width, frame->chroma_height, GL_RED, GL_UNSIGNED_BYTE, frame->data.get() + frame->width * frame->height + frame->chroma_width * frame->chroma_height); + glTextureSubImage2D(tex[2], 0, 0, 0, frame->chroma_width, frame->chroma_height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(frame->width * frame->height + frame->chroma_width * frame->chroma_height)); glGenerateTextureMipmap(tex[2]); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glBindTextureUnit(0, tex[0]); glBindTextureUnit(1, tex[1]); glBindTextureUnit(2, tex[2]); @@ -462,6 +489,15 @@ void VideoWidget::paintGL() glEnd(); } +void VideoWindow::set_current_frame(shared_ptr new_frame) +{ + { + lock_guard lock(current_frame_mu); + current_frame = std::move(new_frame); + } + update(); +} + void matmul3x3(const double a[9], const double b[9], double res[9]) { for (int i = 0; i < 3; ++i) { @@ -506,6 +542,7 @@ void VideoWidget::wheelEvent(QWheelEvent *event) matmul3x3(tmp2, translation_matrix, zoom_matrix); fixup_zoom_matrix(); + video_window->set_zoom_matrix(zoom_matrix); update(); } @@ -542,6 +579,7 @@ void VideoWidget::mouseMoveEvent(QMouseEvent *e) zoom_matrix[6] += dx; zoom_matrix[7] -= dy; fixup_zoom_matrix(); + video_window->set_zoom_matrix(zoom_matrix); last_drag_x = e->position().x(); last_drag_y = e->position().y(); @@ -594,30 +632,40 @@ void VideoWidget::fixup_zoom_matrix() zoom_matrix[7] = std::max(zoom_matrix[7], 1.0 - zoom_matrix[4]); // Top side (y=1). } -void VideoWidget::open(const string &filename) +bool VideoWidget::open(const string &filename) { stop(); internal_rewind(); pathname = filename; play(); + + while (running == STARTING) { + // Poor man's condition variable... + usleep(10000); + sched_yield(); + } + return (running != VIDEO_FILE_ERROR); } void VideoWidget::play() { - if (running) { + if (running != NOT_RUNNING && running != VIDEO_FILE_ERROR) { std::lock_guard lock(queue_mu); command_queue.push_back(QueuedCommand { QueuedCommand::RESUME }); producer_thread_should_quit.wakeup(); return; } - running = true; + running = STARTING; producer_thread_should_quit.unquit(); + if (producer_thread.joinable()) { + producer_thread.join(); + } producer_thread = std::thread(&VideoWidget::producer_thread_func, this); } void VideoWidget::pause() { - if (!running) { + if (running == NOT_RUNNING || running == VIDEO_FILE_ERROR) { return; } std::lock_guard lock(queue_mu); @@ -627,7 +675,7 @@ void VideoWidget::pause() void VideoWidget::seek(int64_t relative_seek_ms) { - if (!running) { + if (running == NOT_RUNNING || running == VIDEO_FILE_ERROR) { return; } std::lock_guard lock(queue_mu); @@ -637,7 +685,7 @@ void VideoWidget::seek(int64_t relative_seek_ms) void VideoWidget::seek_frames(int64_t relative_seek_frames) { - if (!running) { + if (running == NOT_RUNNING || running == VIDEO_FILE_ERROR) { return; } std::lock_guard lock(queue_mu); @@ -647,7 +695,7 @@ void VideoWidget::seek_frames(int64_t relative_seek_frames) void VideoWidget::seek_absolute(int64_t position_ms) { - if (!running) { + if (running == NOT_RUNNING || running == VIDEO_FILE_ERROR) { return; } std::lock_guard lock(queue_mu); @@ -657,10 +705,9 @@ void VideoWidget::seek_absolute(int64_t position_ms) void VideoWidget::stop() { - if (!running) { + if (running == NOT_RUNNING || running == VIDEO_FILE_ERROR) { return; } - running = false; producer_thread_should_quit.quit(); producer_thread.join(); } @@ -669,7 +716,9 @@ void VideoWidget::producer_thread_func() { if (!producer_thread_should_quit.should_quit()) { if (!play_video(pathname)) { - // TODO: Send the error back to the UI somehow. + running = VIDEO_FILE_ERROR; + } else { + running = NOT_RUNNING; } } } @@ -875,6 +924,8 @@ bool VideoWidget::play_video(const string &pathname) internal_rewind(); + running = RUNNING; + // Main loop. int consecutive_errors = 0; double rate = 1.0; @@ -925,11 +976,7 @@ bool VideoWidget::play_video(const string &pathname) bool finished_wakeup; finished_wakeup = producer_thread_should_quit.sleep_until(next_frame_start); if (finished_wakeup) { - Frame *new_frame = new Frame(make_video_frame(frame.get())); - { - lock_guard lock(current_frame_mu); - current_frame.reset(new_frame); - } + video_window->set_current_frame(make_video_frame(frame.get())); last_frame = steady_clock::now(); update(); break; @@ -943,11 +990,7 @@ bool VideoWidget::play_video(const string &pathname) if (paused) { // Just paused, so present the frame immediately and then go into deep sleep. - Frame *new_frame = new Frame(make_video_frame(frame.get())); - { - lock_guard lock(current_frame_mu); - current_frame.reset(new_frame); - } + video_window->set_current_frame(make_video_frame(frame.get())); last_frame = steady_clock::now(); update(); break; @@ -985,9 +1028,77 @@ float compute_chroma_offset(float pos, unsigned subsampling_factor, unsigned res } } -VideoWidget::Frame VideoWidget::make_video_frame(const AVFrame *frame) +shared_ptr VideoWidget::alloc_frame(unsigned width, unsigned height, unsigned chroma_width, unsigned chroma_height) +{ + lock_guard lock(freelist_mu); + for (auto it = frame_freelist.begin(); it != frame_freelist.end(); ++it) { + if ((*it)->width == width && + (*it)->height == height && + (*it)->chroma_width == chroma_width && + (*it)->chroma_height == chroma_height) { + Frame *frame = *it; + frame_freelist.erase(it); + return shared_ptr{frame, free_frame}; + } + } + + Frame *frame = new Frame; + frame->owner = this; + frame->width = width; + frame->height = height; + frame->chroma_width = chroma_width; + frame->chroma_height = chroma_height; + + size_t len = frame->width * frame->height + 2 * frame->chroma_width * frame->chroma_height; + + while (!video_window->isValid()) { + usleep(100000); + } + + // Augh :-) + mutex mu; + condition_variable done_cv; + bool done = false; + + post_to_main_thread([this, &frame, len, &done, &mu, &done_cv]{ + video_window->makeCurrent(); + glCreateBuffers(1, &frame->pbo); + glNamedBufferStorage(frame->pbo, len, nullptr, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT); + frame->data = (uint8_t *)glMapNamedBufferRange(frame->pbo, 0, len, GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT | GL_MAP_PERSISTENT_BIT); + video_window->doneCurrent(); + + lock_guard lock(mu); + done = true; + done_cv.notify_all(); + }); + { + unique_lock lock(mu); + done_cv.wait(lock, [&done]{ return done; }); + } + + return shared_ptr{frame, free_frame}; +} + +void VideoWidget::free_frame(VideoWidget::Frame *frame) +{ + VideoWidget *self = frame->owner; + lock_guard lock(self->freelist_mu); + if (self->frame_freelist.size() >= 16) { + GLuint pbo = frame->pbo; + post_to_main_thread([self, pbo]{ + self->video_window->makeCurrent(); + glUnmapNamedBuffer(pbo); + glDeleteBuffers(1, &pbo); + self->video_window->doneCurrent(); + }); + delete self->frame_freelist.front(); + self->frame_freelist.pop_front(); + } + self->frame_freelist.push_back(frame); +} + +shared_ptr VideoWidget::make_video_frame(const AVFrame *frame) { - Frame video_frame; AVFrameWithDeleter sw_frame; if (frame->format == AV_PIX_FMT_VAAPI || @@ -1028,29 +1139,31 @@ VideoWidget::Frame VideoWidget::make_video_frame(const AVFrame *frame) int linesizes[4] = { 0, 0, 0, 0 }; const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(sws_dst_format); - video_frame.width = frame->width; - video_frame.height = frame->height; - video_frame.chroma_width = AV_CEIL_RSHIFT(int(frame->width), desc->log2_chroma_w); - video_frame.chroma_height = AV_CEIL_RSHIFT(int(frame->height), desc->log2_chroma_h); + shared_ptr video_frame = alloc_frame( + frame->width, + frame->height, + AV_CEIL_RSHIFT(int(frame->width), desc->log2_chroma_w), + AV_CEIL_RSHIFT(int(frame->height), desc->log2_chroma_h)); // We always assume left chroma placement for now. - cbcr_offset[0] = compute_chroma_offset(0.0f, 1 << desc->log2_chroma_w, video_frame.chroma_width); - cbcr_offset[1] = compute_chroma_offset(0.5f, 1 << desc->log2_chroma_h, video_frame.chroma_height); - - size_t len = frame->width * frame->height + 2 * video_frame.chroma_width * video_frame.chroma_height; - video_frame.data.reset(new uint8_t[len]); + video_window->set_cbcr_offset( + compute_chroma_offset(0.0f, 1 << desc->log2_chroma_w, video_frame->chroma_width), + compute_chroma_offset(0.5f, 1 << desc->log2_chroma_h, video_frame->chroma_height) + ); - pic_data[0] = video_frame.data.get(); + pic_data[0] = video_frame->data; linesizes[0] = frame->width; pic_data[1] = pic_data[0] + frame->width * frame->height; - linesizes[1] = video_frame.chroma_width; + linesizes[1] = video_frame->chroma_width; - pic_data[2] = pic_data[1] + video_frame.chroma_width * video_frame.chroma_height; - linesizes[2] = video_frame.chroma_width; + pic_data[2] = pic_data[1] + video_frame->chroma_width * video_frame->chroma_height; + linesizes[2] = video_frame->chroma_width; sws_scale(sws_ctx.get(), frame->data, frame->linesize, 0, frame->height, pic_data, linesizes); + video_frame->need_flush_len = video_frame->width * video_frame->height + 2 * video_frame->chroma_width * video_frame->chroma_height; + return video_frame; }