项目作者: whoissunshijia

项目描述 :
Record game screen and push RTMP in UE4.22
高级语言: C++
项目地址: git://github.com/whoissunshijia/ue4-ffmpeg.git
创建时间: 2020-05-21T16:15:59Z
项目社区:https://github.com/whoissunshijia/ue4-ffmpeg

开源协议:

下载


UE4结合FFmpeg实现录制和推流画面(一)

新改版了一套流程,近日更新

之前,有碰到过需要在游戏中录制画面,或者推流游戏画面的需求,所以这里使用了FFmpeg来帮助做到了这一点.下面简单的把这个流程记录一下,这里先只讨论录制的功能,关于FFmpeg内部的细节就先不说了.

​ 目前的工作流程:

Game,Render,Auido,Encode,共4个线程:

Game:记录累加时间,每隔一定时间(1000毫秒/输入的fps(帧率))去把最近一次记录的渲染画面数据传递给Encode.

Render:传递每帧的画面数据.

Audio:传递音频的数据.

Encode:内部依次调用编码音视频数据函数.

录制启动

UFFmpegDirector启动的时候,目前可以传递下面几个参数:

  • World当前的UWorld
  • OutFileName视频输出保存的路径(这里写本地的路径就存在本地,写rtmp地址就是推流)
  • VideoFilter视频的缩放比例,可以自定义宽高比
  • UseGPU是否使用GPU编码
  • FPS视频的输出帧率
  • VideoBitRate视频的码率
  • AudioDelay音频的延迟时间,我在测试的时候发现音视频会有一定的延迟,暂时发现是UE音频输出的时间问题这个可以根据自己测试的结果来设置
  • SoundVolume音频输出大小,这个是按原素材的音量来调节的.不是按UE输出的音量
  1. int UFFmpegFunctionLibrary::CreateFFmpegDirector(UWorld* World, FString OutFileName, FString VideoFilter, bool UseGPU, int FPS, int VideoBitRate, float AudioDelay, float SoundVolume)
  2. {
  3. UFFmpegDirector* d = NewObject<UFFmpegDirector>();
  4. d->AddToRoot();
  5. d->Initialize_Director(World, OutFileName, UseGPU, VideoFilter, FPS, VideoBitRate, AudioDelay, SoundVolume);
  6. return 1;
  7. }

​ 接下里看Initialize_Director函数:

  1. avfilter_register_all();
  2. av_register_all();
  3. avformat_network_init();
  4. audio_delay = AudioDelay;
  5. video_fps = VideoFps;
  6. Video_Tick_Time = float(1) / float(video_fps);
  7. audio_volume = SoundVolume;
  8. gameWindow = GEngine->GameViewport->GetWindow().Get();
  9. out_width = width = FormatSize_X(gameWindow->GetViewportSize().X);
  10. out_height = height = gameWindow->GetViewportSize().Y;
  11. buff_bgr = (uint8_t *)FMemory::Realloc(buff_bgr, 3 * width *height);
  12. outs[0] = (uint8_t *)FMemory::Realloc(outs[0], 4096);
  13. outs[1] = (uint8_t *)FMemory::Realloc(outs[1], 4096);
  14. FString Scale;
  15. FString Resolution;
  16. FString Str_width;
  17. FString Str_height;
  18. if (VideoFilter.Len() > 0)
  19. {
  20. VideoFilter.Split("=", &Scale, &Resolution);
  21. Resolution.Split(":", &Str_width, &Str_height);
  22. out_width= FCString::Atoi(*Str_width);
  23. out_height= FCString::Atoi(*Str_height);
  24. }
  25. filter_descr.Append("[in]");
  26. filter_descr.Append("scale=");
  27. filter_descr.Append(FString::FromInt(out_width));
  28. filter_descr.Append(":");
  29. filter_descr.Append(FString::FromInt(out_height));
  30. filter_descr.Append("[out]");
  31. int IsUseRTMP = OutFileName.Find("rtmp");
  32. if (IsUseRTMP==0)
  33. {
  34. if (avformat_alloc_output_context2(&out_format_context, NULL, "flv", TCHAR_TO_ANSI(*OutFileName)) < 0)
  35. {
  36. check(false);
  37. }
  38. }
  39. else
  40. {
  41. if (avformat_alloc_output_context2(&out_format_context, NULL, NULL, TCHAR_TO_ANSI(*OutFileName)) < 0)
  42. {
  43. check(false);
  44. }
  45. }
  46. //create audio encoder
  47. Create_Audio_Swr();
  48. Create_Audio_Encoder("aac");
  49. //create video encoder
  50. Create_Video_Encoder(UseGPU, TCHAR_TO_ANSI(*OutFileName), VideoBitRate);
  51. Alloc_Video_Filter();
  52. //create encode thread
  53. CreateEncodeThread();
  54. //bind delegate for get video data and audio data
  55. Begin_Receive_VideoData();
  56. Begin_Receive_AudioData(World);
  57. //End PIE deleate and tick delegate
  58. AddEndFunction();
  59. AddTickFunction();
  • 前三句avfilter_register_all,av_register_all,avformat_network_initFFmpeg初始化的一些操作

  • 后面根据当前打开的窗口,获取窗口的宽高数值.还有分析当前是本地存储视频,还是rtmp推流

  • Create_Audio_Swr这个函数是初始化一个音频转换的格式,这里注意的一点是UE所有的音频输出都是按照48khz这个采样率,输出所以内部我也直接写成按照48kzh这个输入采样率来转换输出.所以如果要是接入外部音频的话,这里需要把in_sample_rate48000换成实际需要的.

    1. swr = swr_alloc();
    2. av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    3. av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    4. av_opt_set_int(swr, "in_sample_rate", 48000, 0);
    5. av_opt_set_int(swr, "out_sample_rate", 48000, 0);
    6. av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
    7. av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
    8. swr_init(swr);
  • Create_Audio_Encoder("aac")这个函数是创建了音频编码器,编码格式为aac

  • Create_Video_Encoder这个函数是创建视频编码器,这里需要注意的一下是编码器参数:

    1. video_encoder_codec_context->width = out_width;
    2. video_encoder_codec_context->height = out_height;
    3. video_encoder_codec_context->max_b_frames = 2;
    4. video_encoder_codec_context->time_base.num = 1;
    5. video_encoder_codec_context->time_base.den = video_fps;
    6. video_encoder_codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
    7. video_encoder_codec_context->me_range = 16;
    8. video_encoder_codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
    9. video_encoder_codec_context->profile = FF_PROFILE_H264_BASELINE;
    10. video_encoder_codec_context->frame_number = 1;
    11. video_encoder_codec_context->qcompress = 0.8;
    12. video_encoder_codec_context->max_qdiff = 4;
    13. video_encoder_codec_context->level = 30;
    14. video_encoder_codec_context->gop_size = 25;
    15. video_encoder_codec_context->qmin = 18;
    16. video_encoder_codec_context->qmax = 28;
    17. video_encoder_codec_context->me_range = 16;
    18. video_encoder_codec_context->framerate = { video_fps,1 };

    qminqmax这两个关乎输出视频的质量,取值在0-51之间,0表示质量最好,反之是质量最差,这两个值可以根据实际需求来设置.

  • Alloc_Video_Filter这个是创建视频的过滤器,视频的缩放,就是靠这个来实现,后续可以再添加水印等功能,如果有需要的话.

  • CreateEncodeThread这个函数创建了一个编码的线程,这里说明一下,现在编码视频的方式是,先拿到当前帧的数据,然后拷贝出来,把数据转化成另外的格式,再发送给编码器去编码,由于这个过程比较耗时,如果放在游戏或者渲染线程内就很影响帧率,所以这里另外用了一个线程,把流程简化到,只把拷贝当前视频帧这个操作放在了渲染线程,后续的操作用另外的线程去做,这样就大大减少了占用渲染线程的时间.

    1. Runnable = new FEncoderThread();
    2. Runnable->CreateQueue(4 * width*height, 2048 * sizeof(float), 30, 40);
    3. Runnable->GetAudioProcessDelegate().BindUObject(this, &UFFmpegDirector::Encode_Audio_Frame);
    4. Runnable->video_encode_delegate.BindUObject(this, &UFFmpegDirector::Encode_Video_Frame);
    5. Runnable->GetAudioTimeProcessDelegate().BindUObject(this, &UFFmpegDirector::Encode_SetCurrentAudioTime);
    6. RunnableThread = FRunnableThread::Create(Runnable, TEXT("EncoderThread"));
    • CreateQueue(4 * width*height, 2048 * sizeof(float), 30, 40);这里的四个参数:
      • 4 * width*height这个是告诉编码线程内部的视频缓存队列,当前每个帧所需要的大小,4的原因是拿到的UE的画面帧的格式是A2R10G10B104个字节
      • 2048 * sizeof(float)其中的2048UE音频格式的双声道的采样个数,每个声道1024个,存储的数据类型是float
      • 3040分别是视音频的缓存队列大小
    • Runnable->GetAudioProcessDelegate().BindUObject(this, &UFFmpegDirector::Encode_Audio_Frame);这个是绑定的编码视频数据的函数
    • Runnable->video_encode_delegate.BindUObject(this, &UFFmpegDirector::Encode_Video_Frame);这个是绑定的编码音频的函数
    • Runnable->GetAudioTimeProcessDelegate().BindUObject(this, &UFFmpegDirector::Encode_SetCurrentAudioTime);这个是绑定的获取当前播放当前音频的时间
  • Begin_Receive_VideoData();这个函数是绑定当前窗口每帧画面渲染的结果.使用OnBackBufferReady_RenderThread来接受

  • Begin_Receive_AudioData(World);这个函数是注册了一个音频数据的监听,可以获取到当前正在输出的音频数据.

  • AddEndFunction();AddTickFunction();分别是绑定结束时的调用和为当前对象增加Tick

视频编码

​ 先看OnBackBufferReady_RenderThread

  1. void UFFmpegDirector::OnBackBufferReady_RenderThread(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
  2. {
  3. if (gameWindow == &SlateWindow)
  4. {
  5. if (ticktime >= Video_Tick_Time)
  6. {
  7. GameTexture = BackBuffer;
  8. ticktime -= Video_Tick_Time;
  9. GetScreenVideoData();
  10. }
  11. }
  12. }

​ 由于不同的PIE模式,渲染的窗口可能不止一个,所以这里有一个判断gameWindow == &SlateWindow,只接受创建时窗口的数据,Video_Tick_Time是根据最开始传入的FPS帧率来计算的一个时间间隔.ticktime是在Tick函数内递增的一个值,简单的说,就是如果当前帧传入的时间已经到了要编码的时候,就记录当前的视频帧数据,数据的记录就依靠GetScreenVideoData();这个函数:

  1. FRHICommandListImmediate& list = GRHICommandList.GetImmediateCommandList();
  2. uint8* TextureData = (uint8*)list.LockTexture2D(GameTexture->GetTexture2D(), 0, EResourceLockMode::RLM_ReadOnly, LolStride, false);
  3. if(Runnable)
  4. Runnable->InsertVideo(TextureData);
  5. list.UnlockTexture2D(GameTexture, 0, false);

​ 这个只做了一个功能,把当前画面的数据,传递给编码线程内部的视频数据缓存,数据拷贝完成以后,就结束,渲染线程继续工作.

​ 编码线程接收到数据的时候,就会根据音视频的不同,调用不同的编码函数,视频这里调用的是Encode_Video_Frame,把从渲染线程拷贝的数据,再传递出来,具体操作可以看Encode_Video_Frame内部,这里有一个地方说明一下:

  1. for (Row = 0; Row < height; ++Row)
  2. {
  3. uint32* PixelPtr = (uint32*)TextureDataPtr;
  4. for (Col = 0; Col < width; ++Col)
  5. {
  6. uint32 EncodedPixel = *PixelPtr;
  7. // AV_PIX_FMT_BGR24 这里暂时转换为BGR
  8. // AV_PIX_FMT_RGB24 掉帧严重 暂时不知道为什么
  9. *(buff_bgr + 2) = (EncodedPixel >> 2) & 0xFF;
  10. *(buff_bgr + 1) = (EncodedPixel >> 12) & 0xFF;
  11. *(buff_bgr) = (EncodedPixel >> 22) & 0xFF;
  12. buff_bgr += 3;
  13. ++PixelPtr;
  14. }
  15. TextureDataPtr += LolStride;
  16. }

​ 由于UE的每帧的像素格式数据是A2R10G10B10,FFmpeg并没有对应的这一格式转换,所以这里,在损失了一定的精度下,暴力转换了一下,把A2丢弃,剩下的RGB拿最高的8位,组成了B8G8R8,去给FFmpeg编码,到此,一帧的画面,从UE到最后输出视频,算是编码完成

音频编码

​ 音频的数据的获取在

  1. void UFFmpegDirector::OnNewSubmixBuffer(const USoundSubmix* OwningSubmix, float* AudioData, int32 NumSamples, int32 NumChannels, const int32 SampleRate, double AudioClock)
  2. {
  3. if(Runnable)
  4. Runnable->InsertAudio((uint8_t*)AudioData, (uint8_t*)&AudioClock);
  5. }

​ 这里拿到数据以后,分别把,音频数据和当前音频播放时间传递给了编码线程.之后,编码线程,会把音频数据再传递给Encode_Audio_Frame函数来进行编码

Encode_Audio_Frame函数内部,有一个地方说明一下:

  1. if (got_output)
  2. {
  3. audio_pkt->pts = audio_pkt->dts = av_rescale_q(
  4. (CurrentAuidoTime + audio_delay) / av_q2d({ 1,48000 }),
  5. { 1,48000 },
  6. out_audio_stream->time_base);
  7. audio_pkt->duration = av_rescale_q(
  8. audio_pkt->duration,
  9. { 1,48000 },
  10. out_audio_stream->time_base);
  11. audio_pkt->stream_index = audio_index;
  12. av_write_frame(out_format_context, audio_pkt);
  13. av_packet_unref(audio_pkt);
  14. }

​ 这里有一个视频数据内部的时间转换,由于UE都是输出48khz,所以这里没有写成可变的,如果使用的时候,音频是从外部传入的,需要修改(CurrentAuidoTime + audio_delay) / av_q2d({ 1,48000 })内部的48000还有将Create_Audio_Swr内的av_opt_set_int(swr, "in_sample_rate", 48000, 0);同样修改,换成实际需求的即可.其中CurrentAuidoTime是当前音频的播放时间

github:https://github.com/whoissunshijia/ue4-ffmpeg

注意

  • PotPlayer播放器,播放声音会有问题,其他播放器暂时没有发现声音不正常.
  • 如果想要缩放分辨率,可以将参数VideoFilter中的sclae=width:height改成想要的输出分辨率
  • 如果把插件放到自己工程中,需要在VS中把项目

1

​ 后面添加-audiomixer

  • 现在输出的视频分辨率为游戏窗口的大小,输出的视频帧率,码率,分辨率,可以根据性能酌情调整,一般720P的视频,帧率可以设置30,码率2000000,分辨率1280*720.

  • 可以加群453886054交流