Encoding frames to video with ffmpeg

前端 未结 1 496
说谎
说谎 2020-12-18 13:00

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

相关标签:
1条回答
  • 2020-12-18 13:53

    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:

    • To use H264 encoder, find it with AV_CODEC_ID_H264, not AV_CODEC_ID_MPEG1VIDEO, also the ffmpeg libs should be built with libx264.
      • Or, if you have a working nvidia card with nvenc support, 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
    }
    
    0 讨论(0)
提交回复
热议问题