PAYATHON 2026

LibVLC 报 Failed to create video converter,如何排查?

数据阿明

结论

可以直接确认,代码中存在显示逻辑错误:mReadyToDisplay 初始化为 false 后,从未被设为 true,导致 glTexSubImage2D() 不会执行。即使视频转换成功,纹理也不会更新。

OpenGL 操作应移到持有 GLFW context 的渲染线程执行,并为 LibVLC 与渲染线程之间的帧数据增加同步。

Failed to create video converter 需要单独排查。这条日志表明视频转换链创建失败,但仅凭这两行日志,无法判断原因是转换模块缺失、实际加载的运行库不匹配,还是当前解码输出到目标像素格式的转换路径不可用。修正纹理更新代码后,这条错误也不一定消失。

先检查像素格式和完整日志

原来的设置为:

libvlc_video_set_format(
    mMediaPlayer, "RGBA", 1920, 1080, 1920 * 4);

其中 "RGBA" 是 LibVLC 的 FourCC,最后一个参数是每行字节数。对于紧密排列的四字节像素,1920 * 4 与缓冲区大小 1920 * 1080 * 4 在数值上相符。

不能直接断言 LibVLC 不支持 RGBA。VLC 3.0 分支定义了该格式,avcodec 的格式映射中也列出了它,但这不能证明当前 NuGet 部署中的所有转换路径都可用。VLC 格式定义、avcodec 像素格式映射。

先在创建实例时提高日志详细程度:

const char* args[] = { "--verbose=2" };
mLibvlc = libvlc_new(1, args);
if (!mLibvlc)
    throw std::runtime_error("libvlc_new failed");

记录从播放开始到报错之间的完整日志,重点查看实际解码输出格式、请求的输出格式,以及转换模块加载或初始化失败的信息。如果 Windows GUI 程序看不到日志输出,可以通过 libvlc_log_set() 收集。

也可以单独将目标格式改为 "RV32" 做对照。LibVLC 3.0 的 API 文档将它列为 FourCC 示例,但更换格式后,OpenGL 上传时的通道顺序也必须与实际输出一致,不能继续默认按 RGBA 解释。LibVLC 3.0 视频接口。

MP4 和 MKV 是容器格式。更换容器未必会改变内部视频编码或解码后的像素格式,因此两者报错相同,仍不足以定位原因。

将纹理上传移到渲染线程

程序只在创建窗口的线程调用了:

glfwMakeContextCurrent(GLFWwindow);

不能据此假定 LibVLC 视频回调也拥有该 context。GLFW 要求执行 OpenGL 调用的线程具有 current context,而且同一个 context 同时只能在一个线程上处于 current 状态。GLFW context 说明。

因此,不要在 vlcDisplayCallback() 中直接执行 glBindTexture() 和 glTexSubImage2D()。可以采用以下流程:

  1. lock 回调为 LibVLC 提供写入用的缓冲区。
  2. unlock 回调将已完成的像素复制到待上传缓冲区。
  3. 主循环取得一份稳定的帧数据,再上传到 OpenGL texture。

普通 bool 和 volatile bool 都无法保护整帧像素。即使将标志改为 std::atomic<bool>,也要避免 LibVLC 写入像素时,渲染线程正在读取同一块内存。

修改缓冲区和回调

下面的排查实现适用于当前固定的 1920×1080、RGBA 输出。它使用最新解码帧验证显示链路,可能跳过尚未上传的旧帧。若要严格按照播放时钟呈现并处理音画同步,应进一步使用按 picture 管理的帧队列和 display 回调。

先补充头文件及回调声明:

#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include <stdexcept>

void* vlcLockCallback(void* opaque, void** planes);
void vlcUnlockCallback(
    void* opaque, void* picture, void* const* planes);

用以下成员替换原来的 mPixelBuffer 和 mReadyToDisplay,放在 VLC_Data 的 public 区域:

enum : unsigned {
    Width = 1920,
    Height = 1080,
    Pitch = Width * 4
};

std::vector<uint8_t> mDecodeStorage;
uint8_t* mDecodePixels = nullptr;
std::vector<uint8_t> mPendingFrame;
std::vector<uint8_t> mUploadFrame;
std::mutex mFrameMutex;
bool mFrameReady = false;

在构造函数中,替换原来的 mPixelBuffer.resize(...):

const size_t frameBytes = size_t(Pitch) * Height;

// Extra space allows a 32-byte-aligned address inside the vector.
mDecodeStorage.resize(frameBytes + 31);
void* pixels = mDecodeStorage.data();
size_t space = mDecodeStorage.size();
mDecodePixels = static_cast<uint8_t*>(
    std::align(32, frameBytes, pixels, space));
if (!mDecodePixels)
    throw std::runtime_error("Pixel buffer alignment failed");

mPendingFrame.resize(frameBytes);
mUploadFrame.resize(frameBytes);

LibVLC 3.0 文档要求 lock 回调返回的像素平面起始地址按 32 字节对齐,而普通 std::vector<uint8_t> 的接口没有提供这项保证。上述分配完成后,不要再调整 mDecodeStorage 的容量,以免使 mDecodePixels 失效。像素平面对齐要求。

将回调注册改为:

libvlc_video_set_format(
    mMediaPlayer, "RGBA", Width, Height, Pitch);
libvlc_video_set_callbacks(
    mMediaPlayer,
    vlcLockCallback,
    vlcUnlockCallback,
    nullptr,
    this);

删除原来的 vlcDisplayCallback(),使用下面两个回调:

void* vlcLockCallback(void* opaque, void** planes)
{
    auto* data = static_cast<VLC_Data*>(opaque);
    planes[0] = data->mDecodePixels;
    return nullptr;
}

void vlcUnlockCallback(
    void* opaque, void* picture, void* const* planes)
{
    (void)picture;
    auto* data = static_cast<VLC_Data*>(opaque);

    std::lock_guard<std::mutex> guard(data->mFrameMutex);
    std::memcpy(
        data->mPendingFrame.data(),
        planes[0],
        data->mPendingFrame.size());
    data->mFrameReady = true;
}

unlock 表示像素已经完成写入,可供应用读取;display 对应按播放时钟显示画面的时机。本示例选择在 unlock 时发布帧,以便先确认解码、内存交接和纹理上传是否正常。回调时序说明。

然后在 VLC_Data 中加入只由渲染线程调用的方法:

void UploadLatestFrame()
{
    {
        std::lock_guard<std::mutex> guard(mFrameMutex);
        if (!mFrameReady)
            return;

        mPendingFrame.swap(mUploadFrame);
        mFrameReady = false;
    }

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, mTextureHandle);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
    glTexSubImage2D(
        GL_TEXTURE_2D, 0, 0, 0,
        Width, Height,
        GL_RGBA, GL_UNSIGNED_BYTE,
        mUploadFrame.data());
}

交换缓冲区后,LibVLC 回调只更新 mPendingFrame,主线程只读取 mUploadFrame,OpenGL 上传期间不需要一直持有互斥锁。该代码沿用当前程序未使用 Pixel Unpack Buffer 的前提。

主循环改为:

glUseProgram(shader.mHandle);
glUniform1i(
    glGetUniformLocation(shader.mHandle, "diffuseTextureID"), 0);

while (!glfwWindowShouldClose(window.GLFWwindow) &&
       glfwGetKey(window.GLFWwindow, GLFW_KEY_ESCAPE) != GLFW_PRESS)
{
    vlcData.UploadLatestFrame();
    window.ClearBuffers();
    quadModel.Draw();
    window.SwapBufferAndPollEvents();
}

修正播放调用和资源释放

原来的 Play() 把实际播放操作放进了 assert():

assert(libvlc_media_player_play(mMediaPlayer) != -1);

定义 NDEBUG 后,断言表达式不会执行,播放调用也会随之消失。应改为正常调用并检查返回值:

void Play()
{
    if (libvlc_media_player_play(mMediaPlayer) == -1) {
        const char* error = libvlc_errmsg();
        throw std::runtime_error(
            error ? error : "libvlc_media_player_play failed");
    }
}

成功返回不代表后续异步解码和转换一定成功,仍需查看日志。

在销毁回调使用的缓冲区前,先从主线程停止播放器,再释放播放器和实例:

~VLC_Data()
{
    if (mMediaPlayer) {
        libvlc_media_player_stop(mMediaPlayer);
        libvlc_media_player_release(mMediaPlayer);
    }
    if (mLibvlc)
        libvlc_release(mLibvlc);

    glDeleteTextures(1, &mTextureHandle);
}

创建 mLibvlc、mMedia、mMediaPlayer 时,也应分别检查返回指针,并在失败路径释放已经创建的资源。

Window 的释放顺序应调整为先销毁窗口,再终止 GLFW:

~Window()
{
    if (GLFWwindow)
        glfwDestroyWindow(GLFWwindow);
    glfwTerminate();
}

原程序先调用 glfwTerminate(),而该调用会销毁剩余窗口及其 context,之后不应再用原窗口指针执行销毁操作。GLFW 窗口与 context 生命周期。

转换错误仍存在时如何继续定位

根据排查结果选择下一步:

  • 始终进不了 lock 或 unlock 回调:先检查完整转换日志,以及程序实际加载的 libvlc.dll、libvlccore.dll 和插件目录。确认它们来自同一套部署,进程位数也匹配,避免混入其他 VLC 安装的文件。
  • 已经持续收到完整帧,但窗口仍无画面:检查主线程是否执行了 UploadLatestFrame(),再检查 texture 绑定、shader、viewport 和 OpenGL 错误。
  • 只有某个输出 FourCC 失败:保留相同媒体和其他设置,比较两次日志中的格式转换链。若换格式后出现红蓝颠倒,应检查通道顺序;颜色错误与转换器创建失败属于不同阶段的问题。

Failed to set on top 涉及视频输出的置顶操作。这一行本身不足以解释转换器创建失败,也无法解释代码中永远不会进入的纹理上传分支。排查时应分别观察错误日志、视频回调和纹理上传的执行情况。

以上代码针对所给示例修改,未在该 Windows 10 与 NuGet 运行环境中执行验证,不能据此宣称已确定转换失败的唯一根因。

备注:内容仅供参考。