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()。可以采用以下流程:
lock回调为 LibVLC 提供写入用的缓冲区。unlock回调将已完成的像素复制到待上传缓冲区。- 主循环取得一份稳定的帧数据,再上传到 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 运行环境中执行验证,不能据此宣称已确定转换失败的唯一根因。
备注:内容仅供参考。
