Part1前言
InVideo是一款基于虚幻引擎的安防视频播放插件,项目开源地址
https://github.com/inveta/InVideo
最近在使用中发现一个严重的问题:当摄像机不在线的时候,关闭会将整个虚幻蓝图卡主。
Part2问题分析
我们在打开视频的时候,会开启一个线程,详见代码
void UInVideoWidget::StartPlay
但是当视频不在线的时候,我们的run函数会卡主,大概持续时间在30秒左右。
if (false == m_WrapOpenCv->m_Stream.open(TCHAR_TO_UTF8(*m_VideoURL)))
当我们调用关闭的时候由于run函数没有退出,所以会一直阻塞 等待run函数退出。
void UInVideoWidget::StopPlay()
m_Thread->Kill();
这就是导致虚幻引擎蓝图卡主的根本原因。
Part3问题解决
既然找到了原因,我们只需要将void UInVideoWidget::StopPlay()
改为异步执行,是不是就解决了呢?其实这里非常麻烦。需要解决以下几个问题:
1问题1
关闭完 再打开,所有变量是共享的,所以必须等待StopPlay执行成功才能进行后续的请求。
为了解决这个问题,我们需要将所有成员变量单独保存,每次打开都采用新的变量。另外还需要保持每次打开的线程都是重新开启的。
所以我们定义一个类,将成员变量全部移入
class VideoPlay :public FRunnable
{
public:
void StartPlay(const FString VideoURL, FDelegatePlayFailed Failed, FDelegateFirstFrame FirstFrame,
const bool RealMode = true, const int Fps = 25, UInVideoWidget* widget=nullptr);
void StopPlay();
public:
bool Init() override;
uint32 Run() override;
void Stop() override;
void Exit() override;
private:
void UpdateTexture();
void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData);
void NotifyFailed();
void NotifyFirstFrame();
public:
UTexture2D* VideoTexture = nullptr;
UInVideoWidget* m_widget = nullptr;
private:
FRunnableThread* m_Thread = nullptr;
TAtomic<bool> m_Stopping = false;
FString m_VideoURL;
float m_UpdateTime = 20;
int m_Fps = 25;
bool m_RealMode = true;
float m_SleepSecond = 1 / 50;
FDateTime m_LastReadTime = FDateTime::Now();
FDelegatePlayFailed m_Failed;
FDelegateFirstFrame m_FirstFrame;
bool m_BFirstFrame = false;
class WrapOpenCv
{
public:
cv::VideoCapture m_Stream;
cv::Mat m_Frame;
};
WrapOpenCv* m_WrapOpenCv = nullptr;
FVector2D m_VideoSize = FVector2D(0, 0);
FUpdateTextureRegion2D* m_VideoUpdateTextureRegion = nullptr;
FTexture2DResource* m_Texture2DResource = nullptr;
TArray64<FColor> Data;
};
2问题2
这个对象肯定保存在UInVideoWidget类中,如何对这个对象进行管理呢?比如我们又打开了一个新的,之前的对象如何释放?
对象释放的流程为:停止线程---》释放对象
如果我们来回打开不在线的时候,可能会存在好几个对象需要被释放?那么我们是不是还需要一个队列进行存储呢?还需要涉及多线程同步队列的问题。
这里我们引入一个非常厉害的概念:智能指针。通过智能指针来自动管理这个对象,这样我们就不需要用一个队列来管理指针的释放了。
我们定义一个智能指针,保存已经打开的视频。
TUniquePtr<VideoPlay> m_VideoPlayPtr;
在释放的时候,我们采用MoveTemp方式,将指针的所有权转移,这样就自动进行释放了。为了保证不卡主当前线程,我们采用了UE的异步任务来解决,代码如下
void UInVideoWidget::StopPlay()
{
if (m_VideoPlayPtr.Get() == nullptr)
{
return;
}
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ptr = MoveTemp(m_VideoPlayPtr)]()
{
ptr->StopPlay();
});
}
3问题3
UInVideoWidget和VideoPlay生命周期问题。之前我们将所有业务写在UInVideoWidget类中,这样完全不需要担心生命周期问题,本来就是一个对象。现在遇到的问题是,UInVideoWidget是由虚幻引擎的回收机制来进行回收,而VideoPlay是在一个异步任务里面进行回收,两个完全无法保证同一时刻谁能存在。
为了解决这个问题,我们又引入了一个新的概念,就是将所有和UInVideoWidget对象打交道的地方全部放到GameThread,然后再通过IsValidLowLevel函数检查是否存在。这样就可以确保每次执行的时候都能够知道UInVideoWidget对象是否被释放,示例代码如下
AsyncTask(ENamedThreads::GameThread, [vt = VideoTexture, widget = m_widget]()
{
if (false == widget->IsValidLowLevel())
{
return;
}
if (nullptr == widget->ImageVideo)
{
return;
}
if (false == vt->IsValidLowLevel())
{
return;
}
widget->ImageVideo->SetBrushFromTexture(vt);
});
Part4问题4
UTexture2D的释放问题。之前我们将创建的UTexture2D保存在UInVideoWidget类,这样其生命周期和UInVideoWidget对象一直,但是我们的VideoPlay是一个C++对象,UE无法感知UTexture2D的对象存在,所以创建完之后,就会将其自动释放掉,为了解决这个问题,我们采用了AddtoRoot函数可以防止被虚幻自动释放。示例代码如下
VideoTexture = UTexture2D::CreateTransient(m_VideoSize.X, m_VideoSize.Y);
if (VideoTexture)
{
VideoTexture->UpdateResource();
}
VideoTexture->AddToRoot();
当然在释放的时候,也需要RemoveFromRoot,示例代码如下
AsyncTask(ENamedThreads::GameThread, [vt = VideoTexture]()
{
if (vt->IsValidLowLevel())
{
vt->RemoveFromRoot();
}
});
Part5总结
本文针对invideo关闭视频卡住的问题进行了解决,并解决了在解决这个问题过程发现的另外四个问题:1、成员变量单独提取
2、智能指针自动释放
3、UInVideoWidget和VideoPlay生命周期同步
4、UTexture2D自动回收
Part6Inveta团队
Inveta团队由研发、美术设计、建模等组成。团队介绍:
https://www.inveta.cn/about.html
团队开源项目:
https://github.com/inveta