I am trying to encode a video in Unreal Engine 4 with C++. I have access to the separate frames. Below is the code which reads viewport\'s
displayed pixels and
Not only avcodec_encode_video
is out dated, avcodec_encode_video2
has been tagged with deprecated for a while too. You should use the new avcodec_send_frame
and avcodec_receive_packet
for encoding now.
The "flipping" part doesn't do any good for encoding, and I strongly suggest don't do that in your code. If you find the output size is not right, just switch swscale
interpolation algorithm flag to SWS_ACCURATE_RND
.
Besides old avcodec_encode_video
API, there are several potential risks:
AV_CODEC_ID_H264
, not AV_CODEC_ID_MPEG1VIDEO
, also the ffmpeg libs should be built with libx264
.
avcodec_find_encoder_by_name("h264_nvenc")
will be much better. delete FileHandle
is executed twice.avpicture...
functions has been deprecated for a long time. Use other functions instead.And if performance is critical, move all encoding process to a independent thread instead of the game thread.
I have some codes for encoding UE4 viewport output in my custom GameViewportClient
class, which are similar to ffmpeg official muxing
and encode_video
example.
MyGameViewportClient.h:
UCLASS(Config=Game)
class FUSIONCUT_API UMyGameViewportClient : public UGameViewportClient
{
GENERATED_BODY()
public:
virtual void Draw(FViewport* Viewport, FCanvas* SceneCanvas) override;
void FirstTimeInit();
void InitCodec();
void TidyUp();
void SetAutoRecording(bool val);
void RecordNextFrame();
bool CanRecordNextFrame();
void SetRecording(bool val);
void SetLevelDelay(int32 delay);
void SetOver(bool val);
void SetAbandon(bool val);
void SetFilePath(FString out_file);
void SetThumbnail(FString thumbnail_file, int32 thumbnail_frame);
void SaveThumbnailImage();
private:
UPROPERTY(Config)
FString DeviceNum;
UPROPERTY(Config)
FString H264Crf;
UPROPERTY(Config)
int DeviceIndex;
UPROPERTY()
UFunction* ProgressFunc;
UPROPERTY()
UFunction* FinishFunc;
FIntPoint ViewportSize;
int count;
TArray<FColor> ColorBuffer;
TArray<uint8> IMG_Buffer;
struct OutputStream {
AVStream* Stream;
AVCodecContext* Ctx;
int64_t NextPts;
AVFrame* Frame;
struct SwsContext* SwsCtx;
};
OutputStream VideoSt = { 0 };
AVOutputFormat* Fmt;
AVFormatContext* FmtCtx;
AVCodec* VideoCodec;
AVDictionary* Opt = nullptr;
SwsContext* SwsCtx;
AVPacket Pkt;
int GotOutput;
int InLineSize[1];
bool Start;
bool Over;
bool FirstTime;
bool Abandon;
bool AutoRecording;
bool RecordingNextFrame;
double LastSendingTime;
std::string FilePath;
FString UEFilePath;
int32 LevelDelay;
void EncodeAndWrite();
void CaptureFrame();
void AddStream(enum AVCodecID CodecID);
void OpenVideo();
int WriteFrame(bool need_save_thumbnail = true);
void CloseStream();
void AllocPicture();
int FFmpegEncode(AVFrame *frame);
};
MyGameViewportClient.cpp:
void UMyGameViewportClient::InitCodec()
{
ViewportSize = Viewport->GetSizeXY();
av_register_all();
avformat_alloc_output_context2(&FmtCtx, nullptr, nullptr, FilePath.c_str());
if (!FmtCtx)
{
UE_LOG(LogTemp, Error, TEXT("cannot alloc format context"));
return;
}
Fmt = FmtCtx->oformat;
//auto codec_id = AV_CODEC_ID_H264;
const char codec_name[32] = "h264_nvenc";
//auto codec = avcodec_find_encoder(codec_id);
auto codec = avcodec_find_encoder_by_name(codec_name);
av_format_set_video_codec(FmtCtx, codec);
if (Fmt->video_codec != AV_CODEC_ID_NONE)
{
AddStream(Fmt->video_codec);
}
OpenVideo();
VideoSt.NextPts = 0;
av_dump_format(FmtCtx, 0, FilePath.c_str(), 1);
if (!(Fmt->flags & AVFMT_NOFILE))
{
auto ret = avio_open(&FmtCtx->pb, FilePath.c_str(), AVIO_FLAG_WRITE);
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("Could not open %s: %s"), *UEFilePath, *errstr);
return;
}
}
auto ret = avformat_write_header(FmtCtx, &Opt);
if (ret < 0)
{
UE_LOG(LogTemp, Error, TEXT("Error occurred when writing header to: %s"), *UEFilePath);
return;
}
InLineSize[0] = 4 * VideoSt.Ctx->width;
SwsCtx = sws_getContext(VideoSt.Ctx->width, VideoSt.Ctx->height, AV_PIX_FMT_RGBA,
VideoSt.Ctx->width, VideoSt.Ctx->height, VideoSt.Ctx->pix_fmt,
0, nullptr, nullptr, nullptr);
}
void UMyGameViewportClient::OpenVideo()
{
auto c = VideoSt.Ctx;
AVDictionary* opt = nullptr;
av_dict_copy(&opt, Opt, 0);
auto ret = avcodec_open2(c, VideoCodec, &opt);
av_dict_free(&opt);
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("Could not open video codec: %s"), *errstr);
}
AllocPicture();
if (!VideoSt.Frame)
{
UE_LOG(LogTemp, Error, TEXT("Could not allocate video frame"));
return;
}
if (avcodec_parameters_from_context(VideoSt.Stream->codecpar, c))
{
UE_LOG(LogTemp, Error, TEXT("Could not copy the stream parameters"));
}
}
void UMyGameViewportClient::AllocPicture()
{
VideoSt.Frame = av_frame_alloc();
if (!VideoSt.Frame)
{
UE_LOG(LogTemp, Error, TEXT("av_frame_alloc failed."));
return;
}
VideoSt.Frame->format = VideoSt.Ctx->pix_fmt;
VideoSt.Frame->width = ViewportSize.X;
VideoSt.Frame->height = ViewportSize.Y;
if (av_frame_get_buffer(VideoSt.Frame, 32) < 0)
{
UE_LOG(LogTemp, Error, TEXT("Could not allocate frame data"));
}
}
void UMyGameViewportClient::AddStream(enum AVCodecID CodecID)
{
VideoCodec = avcodec_find_encoder(CodecID);
if (!VideoCodec)
{
UE_LOG(LogTemp, Error, TEXT("Could not find encoder for '%s'"), ANSI_TO_TCHAR(avcodec_get_name(CodecID)));
}
VideoSt.Stream = avformat_new_stream(FmtCtx, nullptr);
if (!VideoSt.Stream)
{
UE_LOG(LogTemp, Error, TEXT("Could not allocate stream"));
}
VideoSt.Stream->id = FmtCtx->nb_streams - 1;
VideoSt.Ctx = avcodec_alloc_context3(VideoCodec);
if (!VideoSt.Ctx)
{
UE_LOG(LogTemp, Error, TEXT("Could not alloc an encoding context"));
}
VideoSt.Ctx->codec_id = CodecID;
VideoSt.Ctx->width = ViewportSize.X;
VideoSt.Ctx->height = ViewportSize.Y;
VideoSt.Stream->time_base = VideoSt.Ctx->time_base = { 1, FRAMERATE };
VideoSt.Ctx->gop_size = 10;
VideoSt.Ctx->max_b_frames = 1;
VideoSt.Ctx->pix_fmt = AV_PIX_FMT_YUV420P;
av_opt_set(VideoSt.Ctx->priv_data, "cq", TCHAR_TO_ANSI(*H264Crf), 0); // change `cq` to `crf` if using libx264
av_opt_set(VideoSt.Ctx->priv_data, "gpu", TCHAR_TO_ANSI(*DeviceNum), 0); // comment this line if using libx264
if (FmtCtx->oformat->flags & AVFMT_GLOBALHEADER)
VideoSt.Ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
void UMyGameViewportClient::EncodeAndWrite()
{
Pkt = { nullptr };
av_init_packet(&Pkt);
fflush(stdout);
IMG_Buffer.SetNum(ColorBuffer.Num() * 4);
uint8* DestPtr = nullptr;
for (auto i = 0; i < ColorBuffer.Num(); i++)
{
DestPtr = &IMG_Buffer[i * 4];
auto SrcPtr = ColorBuffer[i];
*DestPtr++ = SrcPtr.R;
*DestPtr++ = SrcPtr.G;
*DestPtr++ = SrcPtr.B;
*DestPtr++ = SrcPtr.A;
}
uint8* inData[1] = { IMG_Buffer.GetData() };
sws_scale(SwsCtx, inData, InLineSize, 0, VideoSt.Ctx->height, VideoSt.Frame->data, VideoSt.Frame->linesize);
VideoSt.Frame->pts = VideoSt.NextPts++;
if (FFmpegEncode(VideoSt.Frame) < 0)
UE_LOG(LogTemp, Error, TEXT("Error encoding frame %d"), count);
auto ret = WriteFrame();
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("Error while writing video frame: %s"), *errstr);
}
av_packet_unref(&Pkt);
}
int UMyGameViewportClient::WriteFrame()
{
av_packet_rescale_ts(&Pkt, VideoSt.Ctx->time_base, VideoSt.Stream->time_base);
Pkt.stream_index = VideoSt.Stream->index;
return av_interleaved_write_frame(FmtCtx, &Pkt);
}
int UMyGameViewportClient::FFmpegEncode(AVFrame *frame) {
GotOutput = 0;
auto ret = avcodec_send_frame(VideoSt.Ctx, frame);
if (ret < 0 && ret != AVERROR_EOF) {
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Warning, TEXT("error during sending frame, error : %s"), *errstr);
return -1;
}
ret = avcodec_receive_packet(VideoSt.Ctx, &Pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 0;
if (ret < 0)
{
auto errstr = FString(av_make_error_string(ret).c_str());
UE_LOG(LogTemp, Error, TEXT("Error during receiving frame, error : %s"), *errstr);
av_packet_unref(&Pkt);
return -1;
}
GotOutput = 1;
return 0;
}
void UMyGameViewportClient::CloseStream()
{
avcodec_free_context(&VideoSt.Ctx);
av_frame_free(&VideoSt.Frame);
sws_freeContext(SwsCtx);
if (!(Fmt->flags & AVFMT_NOFILE))
{
auto ret = avio_closep(&FmtCtx->pb);
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("avio close failed: %s"), *errstr);
}
}
avformat_free_context(FmtCtx);
}
void UMyGameViewportClient::TidyUp()
{
/* get the delayed frames */
for (GotOutput = 1; GotOutput; count++)
{
fflush(stdout);
FFmpegEncode(nullptr);
if (GotOutput)
{
auto ret = WriteFrame(false);
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("Error while writing video frame: %s"), *errstr);
}
av_packet_unref(&Pkt);
}
}
auto ret = av_write_trailer(FmtCtx);
if (ret < 0)
{
auto errstr = FString(av_err2str(ret));
UE_LOG(LogTemp, Error, TEXT("writing trailer error: %s"), *errstr);
}
CloseStream();
}
void UMyGameViewportClient::Draw(FViewport* Viewport, FCanvas* SceneCanvas)
{
Super::Draw(Viewport, SceneCanvas);
if (Over) // You may need to set this in other class
{
Over = false;
TidyUp();
}
else {
CaptureFrame();
}
}
void UMyGameViewportClient::CaptureFrame()
{
if (!Viewport) {
UE_LOG(LogTemp, Error, TEXT("No viewport"));
return;
}
if (ViewportSize.X == 0 || ViewportSize.Y == 0) {
UE_LOG(LogTemp, Error, TEXT("Viewport size is 0"));
return;
}
ColorBuffer.Empty();
if (!Viewport->ReadPixels(ColorBuffer, FReadSurfaceDataFlags(),
FIntRect(0, 0, ViewportSize.X, ViewportSize.Y)))
{
UE_LOG(LogTemp, Error, TEXT("Cannot read from viewport"));
return;
}
EncodeAndWrite(); // call InitCodec() before this
}