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输出的音量
int UFFmpegFunctionLibrary::CreateFFmpegDirector(UWorld* World, FString OutFileName, FString VideoFilter, bool UseGPU, int FPS, int VideoBitRate, float AudioDelay, float SoundVolume)
{
    UFFmpegDirector* d = NewObject<UFFmpegDirector>();
    d->AddToRoot();
    d->Initialize_Director(World, OutFileName, UseGPU, VideoFilter, FPS, VideoBitRate, AudioDelay, SoundVolume);
    return 1;
}

​ 接下里看Initialize_Director函数:

avfilter_register_all();
av_register_all();
avformat_network_init();

audio_delay = AudioDelay;
video_fps = VideoFps;
Video_Tick_Time = float(1) / float(video_fps);
audio_volume = SoundVolume;

gameWindow = GEngine->GameViewport->GetWindow().Get();

out_width = width = FormatSize_X(gameWindow->GetViewportSize().X);
out_height = height = gameWindow->GetViewportSize().Y;
buff_bgr = (uint8_t *)FMemory::Realloc(buff_bgr, 3 * width *height);
outs[0] = (uint8_t *)FMemory::Realloc(outs[0], 4096);
outs[1] = (uint8_t *)FMemory::Realloc(outs[1], 4096);

FString Scale;
FString Resolution;

FString Str_width;
FString Str_height;
if (VideoFilter.Len() > 0)
{
    VideoFilter.Split("=", &Scale, &Resolution);
    Resolution.Split(":", &Str_width, &Str_height);
    out_width= FCString::Atoi(*Str_width);
    out_height= FCString::Atoi(*Str_height);
}
filter_descr.Append("[in]");
filter_descr.Append("scale=");
filter_descr.Append(FString::FromInt(out_width));
filter_descr.Append(":");
filter_descr.Append(FString::FromInt(out_height));
filter_descr.Append("[out]");

int IsUseRTMP = OutFileName.Find("rtmp");
if (IsUseRTMP==0)
{
    if (avformat_alloc_output_context2(&out_format_context, NULL, "flv", TCHAR_TO_ANSI(*OutFileName)) < 0)
    {
        check(false);
    }
}
else
{
    if (avformat_alloc_output_context2(&out_format_context, NULL, NULL, TCHAR_TO_ANSI(*OutFileName)) < 0)
    {
        check(false);
    }
}
//create audio encoder
Create_Audio_Swr();
Create_Audio_Encoder("aac");

//create video encoder
Create_Video_Encoder(UseGPU, TCHAR_TO_ANSI(*OutFileName), VideoBitRate);
Alloc_Video_Filter();

//create encode thread
CreateEncodeThread();

//bind delegate for get video data and audio data 
Begin_Receive_VideoData();
Begin_Receive_AudioData(World);

//End PIE deleate and tick delegate
AddEndFunction();
AddTickFunction();
  • 前三句avfilter_register_all,av_register_all,avformat_network_initFFmpeg初始化的一些操作
  • 后面根据当前打开的窗口,获取窗口的宽高数值.还有分析当前是本地存储视频,还是rtmp推流
  • Create_Audio_Swr这个函数是初始化一个音频转换的格式,这里注意的一点是UE所有的音频输出都是按照48khz这个采样率,输出所以内部我也直接写成按照48kzh这个输入采样率来转换输出.所以如果要是接入外部音频的话,这里需要把in_sample_rate48000换成实际需要的.
swr = swr_alloc();
av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swr, "in_sample_rate", 48000, 0);
av_opt_set_int(swr, "out_sample_rate", 48000, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
swr_init(swr);
  • Create_Audio_Encoder("aac")这个函数是创建了音频编码器,编码格式为aac
  • Create_Video_Encoder这个函数是创建视频编码器,这里需要注意的一下是编码器参数:
video_encoder_codec_context->width = out_width;
video_encoder_codec_context->height = out_height;
video_encoder_codec_context->max_b_frames = 2;
video_encoder_codec_context->time_base.num = 1;
video_encoder_codec_context->time_base.den = video_fps;
video_encoder_codec_context->pix_fmt = AV_PIX_FMT_YUV420P;
video_encoder_codec_context->me_range = 16;
video_encoder_codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
video_encoder_codec_context->profile = FF_PROFILE_H264_BASELINE;
video_encoder_codec_context->frame_number = 1;
video_encoder_codec_context->qcompress = 0.8;
video_encoder_codec_context->max_qdiff = 4;
video_encoder_codec_context->level = 30;
video_encoder_codec_context->gop_size = 25;
video_encoder_codec_context->qmin = 18;
video_encoder_codec_context->qmax = 28;
video_encoder_codec_context->me_range = 16;
video_encoder_codec_context->framerate = { video_fps,1 };

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

  • Alloc_Video_Filter这个是创建视频的过滤器,视频的缩放,就是靠这个来实现,后续可以再添加水印等功能,如果有需要的话.
  • CreateEncodeThread这个函数创建了一个编码的线程,这里说明一下,现在编码视频的方式是,先拿到当前帧的数据,然后拷贝出来,把数据转化成另外的格式,再发送给编码器去编码,由于这个过程比较耗时,如果放在游戏或者渲染线程内就很影响帧率,所以这里另外用了一个线程,把流程简化到,只把拷贝当前视频帧这个操作放在了渲染线程,后续的操作用另外的线程去做,这样就大大减少了占用渲染线程的时间.
Runnable = new FEncoderThread();
Runnable->CreateQueue(4 * width*height, 2048 * sizeof(float), 30, 40);
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);
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

void UFFmpegDirector::OnBackBufferReady_RenderThread(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
    if (gameWindow == &SlateWindow)
    {
        if (ticktime >= Video_Tick_Time)
        {
            GameTexture = BackBuffer;
            ticktime -= Video_Tick_Time;
            GetScreenVideoData();       
        }
    }
}

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

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

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

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

for (Row = 0; Row < height; ++Row)
{
    uint32* PixelPtr = (uint32*)TextureDataPtr;
    for (Col = 0; Col < width; ++Col)
    {
        uint32 EncodedPixel = *PixelPtr;
        //  AV_PIX_FMT_BGR24    这里暂时转换为BGR
        //  AV_PIX_FMT_RGB24    掉帧严重 暂时不知道为什么
        *(buff_bgr + 2) = (EncodedPixel >> 2) & 0xFF;
        *(buff_bgr + 1) = (EncodedPixel >> 12) & 0xFF;
        *(buff_bgr) = (EncodedPixel >> 22) & 0xFF;
        buff_bgr += 3;
        ++PixelPtr;     
    }
    TextureDataPtr += LolStride;
}

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

音频编码

​ 音频的数据的获取在

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

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

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

if (got_output)
{
    audio_pkt->pts = audio_pkt->dts = av_rescale_q(
        (CurrentAuidoTime + audio_delay) / av_q2d({ 1,48000 }),
        { 1,48000 },
        out_audio_stream->time_base);

    audio_pkt->duration = av_rescale_q(
        audio_pkt->duration,
        { 1,48000 },
        out_audio_stream->time_base);

    audio_pkt->stream_index = audio_index;
    av_write_frame(out_format_context, audio_pkt);
    av_packet_unref(audio_pkt);
}

​ 这里有一个视频数据内部的时间转换,由于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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值