diff --git a/src/platform/linux/pipewire.cpp b/src/platform/linux/pipewire.cpp index 1048b8399ca..a7dfc12bf4c 100644 --- a/src/platform/linux/pipewire.cpp +++ b/src/platform/linux/pipewire.cpp @@ -128,11 +128,17 @@ namespace pipewire { */ struct img_descriptor_t: public egl::img_descriptor_t { ~img_descriptor_t() override { - if (data) { + // Only free buffers this image actually owns. The memory-buffer capture + // path points img->data at the PipeWire staging vector (front_buffer), + // which is owned by pipewire_t -- deleting it here corrupts the heap. + if (data && data_owned) { delete[] data; - data = nullptr; } + data = nullptr; + data_owned = false; } + + bool data_owned = false; ///< Whether img->data is owned by this image and must be freed. }; /** @@ -430,13 +436,17 @@ namespace pipewire { struct spa_buffer *buf = stream_data.current_buffer->buffer; if (buf->datas[0].chunk->size != 0) { - auto *img_descriptor = static_cast(img); + auto *img_descriptor = static_cast(img); fill_img_metadata(img_descriptor, buf); if (buf->datas[0].type == SPA_DATA_DmaBuf) { fill_img_dmabuf(img_descriptor, buf, stream_data); } else { img->data = stream_data.front_buffer->data(); + img_descriptor->data_owned = false; img->row_pitch = stream_data.local_stride; + // NV12 is the only 1-byte-per-pixel format delivered on the memory + // path; every other negotiated format is packed 4 bytes per pixel. + img->pixel_pitch = (stream_data.format.info.raw.format == SPA_VIDEO_FORMAT_NV12) ? 1 : 4; } } @@ -937,6 +947,7 @@ namespace pipewire { img->sequence = 0; img->serial = std::numeric_limitsserial)>::max(); img->data = nullptr; + img->data_owned = false; std::fill_n(img->sd.fds, 4, -1); return img; @@ -1061,7 +1072,20 @@ namespace pipewire { * @return Capture status reported to the streaming pipeline. */ int dummy_img(platf::img_t *img) override { - // Empty images are recognized as dummies by the zero sequence number + // Software encoders convert the dummy image immediately; provide a valid + // (black) buffer instead of leaving img->data null, which makes sws fail + // with EINVAL. The buffer is new[]-allocated and marked as owned so the + // destructor releases it. + if (img->data == nullptr) { + const auto w = img->width; + const auto h = img->height; + if (w > 0 && h > 0) { + img->data = new uint8_t[static_cast(w) * h * 4](); // NOSONAR(cpp:S5025) - buffer is owned by the image and freed by img_descriptor_t's destructor + static_cast(img)->data_owned = true; + img->row_pitch = w * 4; + img->pixel_pitch = 4; + } + } return 0; } diff --git a/src/video.cpp b/src/video.cpp index 5306f37d706..e02fd416615 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -3,6 +3,7 @@ * @brief Definitions for video. */ // standard includes +#include #include #include #include @@ -194,198 +195,189 @@ namespace video { */ util::Either vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); - /** - * @brief FFmpeg software encode device used when no hardware frames are required. - */ - class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t { - public: - /** - * @brief Accept a software frame without additional hardware conversion. - * - * @param img Image or frame object to read from or populate. - * @return Conversion status. - */ - int convert(platf::img_t &img) override { - // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer - bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height); - - // Setup the input frame using the caller's img_t - sws_input_frame->data[0] = img.data; - sws_input_frame->linesize[0] = img.row_pitch; - - // Perform color conversion and scaling to the final size - auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get()); - if (status < 0) { - char string[AV_ERROR_MAX_STRING_SIZE]; - BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + int avcodec_software_encode_device_t::convert(platf::img_t &img) { + // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer + bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height); + + // Detect the actual capture pixel format. PipeWire-based captures (KWin + // screencast / XDG portal) deliver NV12 with 1 byte per pixel, while + // KMS/DMABUF captures deliver BGR0 (4 bytes per pixel). The capture + // backend reports bytes per pixel in img.pixel_pitch; fall back to the row + // pitch heuristic when it is unavailable. + const auto pixel_pitch = img.pixel_pitch > 0 ? img.pixel_pitch : (img.row_pitch / std::max(img.width, 1)); + const auto input_fmt = (pixel_pitch == 1) ? AV_PIX_FMT_NV12 : AV_PIX_FMT_BGR0; + + // The sws context is created with the default BGR0 source format; + // recreate it once if the capture is actually NV12. + if (input_fmt != sws_src_format) { + sws_src_format = input_fmt; + if (reinit_sws(input_fmt) < 0) { return -1; } + // The colorspace details were applied to the previous sws context. + apply_colorspace(); + } - // If we require aspect ratio padding, copy the output frame into the final padded frame - if (requires_padding) { - auto fmt_desc = av_pix_fmt_desc_get(static_cast(sws_output_frame->format)); - auto planes = av_pix_fmt_count_planes(static_cast(sws_output_frame->format)); - for (int plane = 0; plane < planes; plane++) { - auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h; - auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w; - auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane]; + // Setup the input frame using the caller's img_t + sws_input_frame->data[0] = img.data; + sws_input_frame->linesize[0] = img.row_pitch; + if (input_fmt == AV_PIX_FMT_NV12) { + sws_input_frame->data[1] = img.data + static_cast(img.row_pitch) * img.height; + sws_input_frame->linesize[1] = img.row_pitch; + } else { + sws_input_frame->data[1] = nullptr; + sws_input_frame->linesize[1] = 0; + } + sws_input_frame->data[2] = nullptr; + sws_input_frame->linesize[2] = 0; + sws_input_frame->data[3] = nullptr; + sws_input_frame->linesize[3] = 0; - // Copy line-by-line to preserve leading padding for each row - for (int line = 0; line < sws_output_frame->height >> shift_h; line++) { - memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), static_cast(sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step); - } - } - } + // Perform color conversion and scaling to the final size + auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get()); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } + + // If we require aspect ratio padding, copy the output frame into the final padded frame + if (requires_padding) { + auto fmt_desc = av_pix_fmt_desc_get(static_cast(sws_output_frame->format)); + auto planes = av_pix_fmt_count_planes(static_cast(sws_output_frame->format)); + for (int plane = 0; plane < planes; plane++) { + auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h; + auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w; + auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane]; - // If frame is not a software frame, it means we still need to transfer from main memory - // to vram memory - if (frame->hw_frames_ctx) { - auto status = av_hwframe_transfer_data(frame, sw_frame.get(), 0); - if (status < 0) { - char string[AV_ERROR_MAX_STRING_SIZE]; - BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); - return -1; + // Copy line-by-line to preserve leading padding for each row + for (int line = 0; line < sws_output_frame->height >> shift_h; line++) { + memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), static_cast(sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step); } } + } - return 0; + // If frame is not a software frame, it means we still need to transfer from main memory + // to vram memory + if (frame->hw_frames_ctx) { + auto status = av_hwframe_transfer_data(frame, sw_frame.get(), 0); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } } - /** - * @brief Attach frame resources used by the next conversion or encode operation. - * - * @param frame Video or graphics frame being processed. - * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. - * @return Status from updating frame. - */ - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->frame = frame; + return 0; + } - // If it's a hwframe, allocate buffers for hardware - if (hw_frames_ctx) { - hw_frame.reset(frame); + int avcodec_software_encode_device_t::set_frame(AVFrame *in_frame, AVBufferRef *hw_frames_ctx) { + this->frame = in_frame; - if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { - return -1; - } - } else { - sw_frame.reset(frame); - } + // If it's a hwframe, allocate buffers for hardware + if (hw_frames_ctx) { + hw_frame.reset(in_frame); - return 0; + if (av_hwframe_get_buffer(hw_frames_ctx, in_frame, 0)) { + return -1; + } + } else { + sw_frame.reset(in_frame); } - /** - * @brief Apply the configured colorspace metadata to the active frame. - */ - void apply_colorspace() override { - auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); - sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1, 0, 1 << 16, 1 << 16); - } + return 0; + } - /** - * When preserving aspect ratio, ensure that padding is black - */ - void prefill() { - auto frame = sw_frame ? sw_frame.get() : this->frame; - av_frame_get_buffer(frame, 0); - av_frame_make_writable(frame); - ptrdiff_t linesize[4] = {frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3]}; - av_image_fill_black(frame->data, linesize, static_cast(frame->format), frame->color_range, frame->width, frame->height); - } + void avcodec_software_encode_device_t::apply_colorspace() { + auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); + sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1, 0, 1 << 16, 1 << 16); + } - /** - * @brief Initialize FFmpeg software encoding for the requested codec. - * - * @param in_width In width. - * @param in_height In height. - * @param frame Video or graphics frame being processed. - * @param format Pixel, audio, or protocol format being converted. - * @param hardware Whether the frame is backed by hardware resources. - * @return 0 on success; nonzero or negative platform status on failure. - */ - int init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) { - // If the device used is hardware, yet the image resides on main memory - if (hardware) { - sw_frame.reset(av_frame_alloc()); + void avcodec_software_encode_device_t::prefill() { + auto active_frame = sw_frame ? sw_frame.get() : this->frame; + av_frame_get_buffer(active_frame, 0); + av_frame_make_writable(active_frame); + std::array linesize = {active_frame->linesize[0], active_frame->linesize[1], active_frame->linesize[2], active_frame->linesize[3]}; + av_image_fill_black(active_frame->data, linesize.data(), static_cast(active_frame->format), active_frame->color_range, active_frame->width, active_frame->height); + } - sw_frame->width = frame->width; - sw_frame->height = frame->height; - sw_frame->format = format; - } else { - this->frame = frame; - } + int avcodec_software_encode_device_t::init(int in_width, int in_height, AVFrame *in_frame, AVPixelFormat format, bool hardware) { + // If the device used is hardware, yet the image resides on main memory + if (hardware) { + sw_frame.reset(av_frame_alloc()); - // Fill aspect ratio padding in the destination frame - prefill(); + sw_frame->width = in_frame->width; + sw_frame->height = in_frame->height; + sw_frame->format = format; + } else { + this->frame = in_frame; + } - auto out_width = frame->width; - auto out_height = frame->height; + // Fill aspect ratio padding in the destination frame + prefill(); - // Ensure aspect ratio is maintained - auto scalar = std::fminf(static_cast(out_width) / in_width, static_cast(out_height) / in_height); - out_width = in_width * scalar; - out_height = in_height * scalar; + auto out_width = in_frame->width; + auto out_height = in_frame->height; - sws_input_frame.reset(av_frame_alloc()); - sws_input_frame->width = in_width; - sws_input_frame->height = in_height; - sws_input_frame->format = AV_PIX_FMT_BGR0; + // Ensure aspect ratio is maintained + auto scalar = std::fminf(static_cast(out_width) / in_width, static_cast(out_height) / in_height); + out_width = in_width * scalar; + out_height = in_height * scalar; - sws_output_frame.reset(av_frame_alloc()); - sws_output_frame->width = out_width; - sws_output_frame->height = out_height; - sws_output_frame->format = format; + sws_input_frame.reset(av_frame_alloc()); + sws_input_frame->width = in_width; + sws_input_frame->height = in_height; + sws_input_frame->format = AV_PIX_FMT_BGR0; - // Result is always positive - offsetW = (frame->width - out_width) / 2; - offsetH = (frame->height - out_height) / 2; + sws_output_frame.reset(av_frame_alloc()); + sws_output_frame->width = out_width; + sws_output_frame->height = out_height; + sws_output_frame->format = format; - sws.reset(sws_alloc_context()); - if (!sws) { - return -1; - } + // Result is always positive + offsetW = (in_frame->width - out_width) / 2; + offsetH = (in_frame->height - out_height) / 2; - AVDictionary *options {nullptr}; - av_dict_set_int(&options, "srcw", sws_input_frame->width, 0); - av_dict_set_int(&options, "srch", sws_input_frame->height, 0); - av_dict_set_int(&options, "src_format", sws_input_frame->format, 0); - av_dict_set_int(&options, "dstw", sws_output_frame->width, 0); - av_dict_set_int(&options, "dsth", sws_output_frame->height, 0); - av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0); - av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0); - av_dict_set_int(&options, "threads", config::video.min_threads, 0); - - auto status = av_opt_set_dict(sws.get(), &options); - av_dict_free(&options); - if (status < 0) { - char string[AV_ERROR_MAX_STRING_SIZE]; - BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); - return -1; - } + sws_src_format = AV_PIX_FMT_BGR0; - status = sws_init_context(sws.get(), nullptr, nullptr); - if (status < 0) { - char string[AV_ERROR_MAX_STRING_SIZE]; - BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); - return -1; - } + return reinit_sws(sws_src_format); + } - return 0; + int avcodec_software_encode_device_t::reinit_sws(AVPixelFormat src_format) { + sws_input_frame->format = src_format; + + sws.reset(sws_alloc_context()); + if (!sws) { + return -1; } - // Store ownership when frame is hw_frame - avcodec_frame_t hw_frame; ///< Hw frame. + AVDictionary *options {nullptr}; + av_dict_set_int(&options, "srcw", sws_input_frame->width, 0); + av_dict_set_int(&options, "srch", sws_input_frame->height, 0); + av_dict_set_int(&options, "src_format", src_format, 0); + av_dict_set_int(&options, "dstw", sws_output_frame->width, 0); + av_dict_set_int(&options, "dsth", sws_output_frame->height, 0); + av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0); + av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0); + av_dict_set_int(&options, "threads", config::video.min_threads, 0); + + auto status = av_opt_set_dict(sws.get(), &options); + av_dict_free(&options); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } - avcodec_frame_t sw_frame; ///< Sw frame. - avcodec_frame_t sws_input_frame; ///< Sws input frame. - avcodec_frame_t sws_output_frame; ///< Sws output frame. - sws_t sws; ///< Software scaler used when frames need CPU-side pixel conversion. + status = sws_init_context(sws.get(), nullptr, nullptr); + if (status < 0) { + char string[AV_ERROR_MAX_STRING_SIZE]; + BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status); + return -1; + } - // Offset of input image to output frame in pixels - int offsetW; ///< Offset w. - int offsetH; ///< Offset h. - }; + return 0; + } /** * @brief Enumerates supported flag options. @@ -1399,7 +1391,7 @@ namespace video { }; #endif - static const std::vector encoders { + static const std::vector encoders { #ifndef __APPLE__ &nvenc, #endif diff --git a/src/video.h b/src/video.h index b70ed53b3a0..42cd47d3b84 100644 --- a/src/video.h +++ b/src/video.h @@ -119,6 +119,74 @@ namespace video { */ using img_event_t = std::shared_ptr>>; + /** + * @brief FFmpeg software encode device used when no hardware frames are required. + */ + class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t { + public: + /** + * @brief Convert a captured image into the encoder input representation. + * + * @param img Image or frame object to read from or populate. + * @return Conversion status. + */ + int convert(platf::img_t &img) override; + + /** + * @brief Attach frame resources used by the next conversion or encode operation. + * @note Takes ownership of 'in_frame'. + * + * @param in_frame Video or graphics frame being processed. + * @param hw_frames_ctx FFmpeg hardware frames context associated with the frame. + * @return Status from updating frame. + */ + int set_frame(AVFrame *in_frame, AVBufferRef *hw_frames_ctx) override; + + /** + * @brief Apply the configured colorspace metadata to the active frame. + */ + void apply_colorspace() override; + + /** + * @brief Initialize FFmpeg software encoding for the requested codec. + * + * @param in_width In width. + * @param in_height In height. + * @param in_frame Video or graphics frame being processed. + * @param format Pixel, audio, or protocol format being converted. + * @param hardware Whether the frame is backed by hardware resources. + * @return 0 on success; nonzero or negative platform status on failure. + */ + int init(int in_width, int in_height, AVFrame *in_frame, AVPixelFormat format, bool hardware); + + private: + /** + * @brief When preserving aspect ratio, ensure that padding is black. + */ + void prefill(); + + /** + * @brief (Re)create the software scaler for the given source format. + * + * @param src_format Pixel format of the captured frames. + * @return 0 on success; nonzero on failure. + */ + int reinit_sws(AVPixelFormat src_format); + + // Store ownership when frame is hw_frame + avcodec_frame_t hw_frame; ///< Hw frame. + + avcodec_frame_t sw_frame; ///< Sw frame. + avcodec_frame_t sws_input_frame; ///< Sws input frame. + avcodec_frame_t sws_output_frame; ///< Sws output frame. + sws_t sws; ///< Software scaler used when frames need CPU-side pixel conversion. + AVPixelFormat sws_src_format {AV_PIX_FMT_BGR0}; ///< Source format the sws context was created with. + + // Offset of input image to output frame in pixels + int offsetW; ///< Offset w. + int offsetH; ///< Offset h. + }; + /** * @brief Pixel formats supported by one encoder backend. */ diff --git a/tests/unit/test_video.cpp b/tests/unit/test_video.cpp index 6ad2d3a83a0..9a7ad1fe222 100644 --- a/tests/unit/test_video.cpp +++ b/tests/unit/test_video.cpp @@ -9,6 +9,13 @@ #include #include #include +#include + +// ffmpeg includes +extern "C" { +#include +#include +} // local includes #include @@ -167,3 +174,72 @@ INSTANTIATE_TEST_SUITE_P( std::make_tuple(120, 11988, std::chrono::nanoseconds {8341666}) // 1e9 * 1001 / 120000 ) ); + +/** + * @brief Software encoder converts BGR0 and NV12 frames, including padded strides and + * backends that don't report the pixel pitch. + */ +TEST(SoftwareEncoderConversion, Bgr0AndNv12) { + constexpr int w = 320; + constexpr int h = 240; + + AVFrame *frame = av_frame_alloc(); + ASSERT_NE(frame, nullptr); + frame->width = w; + frame->height = h; + frame->format = AV_PIX_FMT_YUV420P; + + video::avcodec_software_encode_device_t device; + ASSERT_EQ(device.init(w, h, frame, AV_PIX_FMT_YUV420P, false), 0); + // set_frame() takes ownership of the frame; the device frees it on destruction. + ASSERT_EQ(device.set_frame(frame, nullptr), 0); + + // BGR0 frame (4 bytes per pixel) -- the classic KMS/DMABUF capture layout. + std::vector bgr0_buffer(static_cast(w) * h * 4); + platf::img_t bgr0_img {}; + bgr0_img.data = bgr0_buffer.data(); + bgr0_img.width = w; + bgr0_img.height = h; + bgr0_img.row_pitch = w * 4; + bgr0_img.pixel_pitch = 4; + EXPECT_EQ(device.convert(bgr0_img), 0); + + // NV12 frame (1 byte per pixel row pitch, Y plane + interleaved UV) -- the + // layout delivered by PipeWire-based captures (KWin screencast / portal). + std::vector nv12_buffer(static_cast(w) * h + static_cast(w) * h / 2); + platf::img_t nv12_img {}; + nv12_img.data = nv12_buffer.data(); + nv12_img.width = w; + nv12_img.height = h; + nv12_img.row_pitch = w; + nv12_img.pixel_pitch = 1; + EXPECT_EQ(device.convert(nv12_img), 0); + + // Padded-stride NV12 (alignment padding) -- the case the old row_pitch == + // width heuristic misdetected as BGR0, causing out-of-bounds reads. + constexpr int padded_stride = w + 32; + std::vector padded_nv12_buffer(static_cast(padded_stride) * h + static_cast(padded_stride) * h / 2); + platf::img_t padded_nv12_img {}; + padded_nv12_img.data = padded_nv12_buffer.data(); + padded_nv12_img.width = w; + padded_nv12_img.height = h; + padded_nv12_img.row_pitch = padded_stride; + padded_nv12_img.pixel_pitch = 1; + EXPECT_EQ(device.convert(padded_nv12_img), 0); + + // Capture backends that don't report pixel_pitch fall back to deriving it + // from the row pitch (1 byte per pixel = NV12, 4 = BGR0). + platf::img_t fallback_bgr0_img {}; + fallback_bgr0_img.data = bgr0_buffer.data(); + fallback_bgr0_img.width = w; + fallback_bgr0_img.height = h; + fallback_bgr0_img.row_pitch = w * 4; + EXPECT_EQ(device.convert(fallback_bgr0_img), 0); + + platf::img_t fallback_nv12_img {}; + fallback_nv12_img.data = nv12_buffer.data(); + fallback_nv12_img.width = w; + fallback_nv12_img.height = h; + fallback_nv12_img.row_pitch = w; + EXPECT_EQ(device.convert(fallback_nv12_img), 0); +}