对话音视频牛哥:如何设计功能齐全的跨平台低延迟RTMP播放器

news2025/2/27 16:31:53

开发背景

2015年,我们在做移动单兵应急指挥项目的时候,推送端采用了RTMP方案,这在当时算是介入RTMP比较早的了,RTMP推送模块做好以后,我们找了市面上VLC还有Vitamio,来测试整体延迟,实际效果真的不尽人意,大家知道,应急指挥系统,除了稳定性外,对延迟有很高的要求,几秒钟(>3-5秒)的延迟,是我们接受不了的,VLC之类播放器,虽然功能庞大,点播体验可满足大多场景诉求,直播场景确实不尽人意。

为此,我们萌生了开发个适应低延迟场景下RTMP播放器的想法,并从Windows平台着手,考虑到现有开源播放器大而全的设计,并不适应直播场景,加之时间充裕,我们开始着手自研框架的RTMP播放器设计,初版发布,延迟已在毫秒级,这在当时,哪怕是现在,确实是值得欣慰的一件事。

整体方案架构

RTMP直播播放器,目标很明确,从RTMP服务器(自建服务器或CDN)拉取流数据,完成数据解析、解码、音视频数据同步、绘制工作。

具体对应下图“接收端”部分:

首版设计目标

  • 自有框架,易于扩展;
  • 支持各种异常网络状态处理,如断网重连等;
  • 有Event状态回调,确保开发者可以了解到播放端整体的状态;
  • 支持多实例播放;
  • 视频支持H.264,音频支持AAC/PCMA/PCMU;
  • 支持缓冲时间设置(buffer time);
  • 支持音视频同步;
  • 支持实时静音。

经过迭代后的功能

  • [支持播放协议]RTMP毫秒级延迟(低延迟下200-400ms);
  •  [多实例播放]支持多实例播放(CPU占用更低);
  •  [事件回调]支持网络状态、buffer状态等回调;
  •  [视频格式]支持RTMP扩展H.265,H.264;
  •  [音频格式]支持AAC/PCMA/PCMU/Speex;
  •  [H.264/H.265软解码]支持H.264/H.265软解;
  •  [H.264硬解码]Windows/Android/iOS支持H.264硬解;
  •  [H.265硬解]Windows/Android/iOS支持H.265硬解;
  •  [H.264/H.265硬解码]Android支持设置Sur face模式硬解和普通模式硬解码;
  •  [缓冲时间设置]支持buffer time设置;
  •  [首屏秒开]支持首屏秒开模式(RTMP服务器缓存GOP的情况);
  •  [低延迟模式]支持超低延迟模式设置(公网200~400ms);
  •  [复杂网络处理]支持断网重连等各种网络环境自动适配;
  •  [快速切换URL]支持播放过程中,快速切换其他URL,内容切换更快;
  •  [音视频多种render机制]Android平台,视频:sur faceview/OpenGL ES,音频:AudioTrack/OpenSL ES;
  •  [实时静音]支持播放过程中,实时静音/取消静音;
  •  [实时音量调节]支持播放过程中,实时调节播放音量,调节范围[0, 100];
  •  [实时快照]支持播放过程中截取当前视频帧画面;
  •  [只播关键帧]Windows平台支持实时设置是否只播放关键帧;
  •  [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  •  [渲染镜像]支持水平反转、垂直反转模式设置;
  •  [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  •  [ARGB叠加]Windows平台支持ARGB图像叠加到显示视频(参看C++的DEMO);
  •  [解码前视频数据回调]支持H.264/H.265数据回调;
  •  [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  •  [解码后视频数据缩放回调]Windows平台支持指定回调图像大小的接口(可以对原视图像缩放后再回调到上层);
  •  [解码前音频数据回调]支持AAC/PCMA/PCMU/SPEEX数据回调;
  •  [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  •  [扩展录像功能]支持RTMP H.264、扩展H.265流录制,支持PCMA/PCMU/Speex转AAC后录制,支持设置只录制音频或视频等;

接口设计

Windows平台我们是C接口,对外提供C++和C#调用示例,本文就以C++的demo为例,大概介绍下常用的接口设计。 

1. Init/UnInit()接口

Init和UnInit接口,在多个播放实例启动的时候,也仅需调用一次,做基础的初始化/反初始化操作。

/*
flag目前传0,后面扩展用, pReserve传NULL,扩展用,
成功返回 NT_ERC_OK
*/NT_UINT32(NT_API *Init)(NT_UINT32 flag, NT_PVOID pReserve);

/*
这个是最后一个调用的接口
成功返回 NT_ERC_OK
*/NT_UINT32(NT_API *UnInit)();

2. Open/Close()接口

Open接口的目的,主要是创建实例,正常返回player实例句柄,如有多路播放诉求,创建多个实例即可。

Close接口,和Open()接口对应,负责释放相应实例的资源,调用Close()接口后,记得实例句柄置0。

注意:比如一个实例既可以实现播放,又可同时录像,亦或拉流(转发),这种情况下,调Close()接口时,需要确保录像、拉流都正常停止后,再调用。

/*
flag目前传0,后面扩展用, pReserve传NULL,扩展用,
NT_HWND hwnd, 绘制画面用的窗口, 可以设置为NULL
获取Handle
成功返回 NT_ERC_OK
*/NT_UINT32(NT_API *Open)(NT_PHANDLE pHandle, NT_HWND hwnd, NT_UINT32 flag, NT_PVOID pReserve);

/*
调用这个接口之后handle失效,
成功返回 NT_ERC_OK
*/NT_UINT32(NT_API *Close)(NT_HANDLE handle);

3. 网络状态回调

一个好的播放器,好的状态回调必不可少,比如网络连通状态、快照、录像状态、当前下载速度等实时反馈,可以让上层开发者更好的掌控播放端状态,给用户更好的播放体验。

/*
设置事件回调,如果想监听事件的话,建议调用Open成功后,就调用这个接口
*/NT_UINT32(NT_API *SetEventCallBack)(NT_HANDLE handle,
    NT_PVOID call_back_data, NT_SP_SDKEventCallBack call_back);

demo实现实例:

LRESULT CSmartPlayerDlg::OnSDKEvent(WPARAM wParam, LPARAM lParam){
    if (!is_playing_ && !is_recording_)
    {
        return S_OK;
    }

    NT_UINT32 event_id = (NT_UINT32)(wParam);

    if ( NT_SP_E_EVENT_ID_PLAYBACK_REACH_EOS == event_id )
    {
        StopPlayback();
        return S_OK;
    }
    elseif ( NT_SP_E_EVENT_ID_RECORDER_REACH_EOS == event_id )
    {
        StopRecorder();
        return S_OK;
    }
    elseif ( NT_SP_E_EVENT_ID_RTSP_STATUS_CODE == event_id )
    {
        int status_code = (int)lParam;
        if ( 401 == status_code )
        {
            HandleVerification();
        }

        return S_OK;
    }
    elseif (NT_SP_E_EVENT_ID_NEED_KEY == event_id)
    {
        HandleKeyEvent(false);

        return S_OK;
    }
    elseif (NT_SP_E_EVENT_ID_KEY_ERROR == event_id)
    {
        HandleKeyEvent(true);

        return S_OK;
    }
    elseif ( NT_SP_E_EVENT_ID_PULLSTREAM_REACH_EOS == event_id )
    {
        if (player_handle_ != NULL)
        {
            player_api_.StopPullStream(player_handle_);
        }

        return S_OK;
    }
    elseif ( NT_SP_E_EVENT_ID_DURATION == event_id )
    {
        NT_INT64 duration = (NT_INT64)(lParam);

        edit_duration_.SetWindowTextW(GetHMSMsFormatStr(duration, false, false).c_str());

        return S_OK;
    }

    if ( NT_SP_E_EVENT_ID_CONNECTING == event_id
        || NT_SP_E_EVENT_ID_CONNECTION_FAILED == event_id
        || NT_SP_E_EVENT_ID_CONNECTED == event_id
        || NT_SP_E_EVENT_ID_DISCONNECTED == event_id
        || NT_SP_E_EVENT_ID_NO_MEDIADATA_RECEIVED == event_id)
    {
        if ( NT_SP_E_EVENT_ID_CONNECTING == event_id )
        {
            OutputDebugStringA("connection status: connecting\r\n");
        }
        elseif ( NT_SP_E_EVENT_ID_CONNECTION_FAILED == event_id )
        {
            OutputDebugStringA("connection status: connection failed\r\n");
        }
        elseif ( NT_SP_E_EVENT_ID_CONNECTED == event_id )
        {
            OutputDebugStringA("connection status: connected\r\n");
        }
        elseif (NT_SP_E_EVENT_ID_DISCONNECTED == event_id)
        {
            OutputDebugStringA("connection status: disconnected\r\n");
        }
        elseif (NT_SP_E_EVENT_ID_NO_MEDIADATA_RECEIVED == event_id)
        {
            OutputDebugStringA("connection status: no mediadata received\r\n");
        }

        connection_status_ = event_id;
    }

    if ( NT_SP_E_EVENT_ID_START_BUFFERING == event_id
        || NT_SP_E_EVENT_ID_BUFFERING == event_id
        || NT_SP_E_EVENT_ID_STOP_BUFFERING == event_id )
    {
        buffer_status_ = event_id;
        
        if ( NT_SP_E_EVENT_ID_BUFFERING == event_id )
        {
            buffer_percent_ = (NT_INT32)lParam;

            std::wostringstream ss;
            ss << L"buffering:" << buffer_percent_ << "%";
            OutputDebugStringW(ss.str().c_str());
            OutputDebugStringW(L"\r\n");
        }
    }

    if ( NT_SP_E_EVENT_ID_DOWNLOAD_SPEED == event_id )
    {
        download_speed_ = (NT_INT32)lParam;

        /*std::wostringstream ss;
        ss << L"downloadspeed:" << download_speed_ << L"\r\n";

        OutputDebugStringW(ss.str().c_str());*/
    }

    CString show_str = base_title_;

    if ( connection_status_ != 0 )
    {
        show_str += _T("--链接状态: ");

        if ( NT_SP_E_EVENT_ID_CONNECTING == connection_status_ )
        {
            show_str += _T("链接中");
        }
        elseif ( NT_SP_E_EVENT_ID_CONNECTION_FAILED == connection_status_ )
        {
            show_str += _T("链接失败");
        }
        elseif ( NT_SP_E_EVENT_ID_CONNECTED == connection_status_ )
        {
            show_str += _T("链接成功");
        }
        elseif ( NT_SP_E_EVENT_ID_DISCONNECTED == connection_status_ )
        {
            show_str += _T("链接断开");
        }
        elseif (NT_SP_E_EVENT_ID_NO_MEDIADATA_RECEIVED == connection_status_)
        {
            show_str += _T("收不到数据");
        }
    }

    if (download_speed_ != -1)
    {
        std::wostringstream ss;
        ss << L"--下载速度:" << (download_speed_ * 8 / 1000) << "kbps"
          << L"(" << (download_speed_ / 1024) << "KB/s)";

        show_str += ss.str().c_str();
    }

    if ( buffer_status_ != 0 )
    {
        show_str += _T("--缓冲状态: ");

        if ( NT_SP_E_EVENT_ID_START_BUFFERING == buffer_status_ )
        {
            show_str += _T("开始缓冲");
        }
        elseif (NT_SP_E_EVENT_ID_BUFFERING == buffer_status_)
        {
            std::wostringstream ss;
            ss << L"缓冲中" << buffer_percent_ << "%";
            show_str += ss.str().c_str();
        }
        elseif (NT_SP_E_EVENT_ID_STOP_BUFFERING == buffer_status_)
        {
            show_str += _T("结束缓冲");
        }
    }


    SetWindowText(show_str);

    return S_OK;
}

4. 软解码还是硬解码?

一般来说,Windows平台如果同时播放的实例不多或者分辨率不是太高的话,考虑到播放体验,建议优先考虑软解码,如果特定设备需要多路播放,也可以考虑硬解,需要注意的是,如果调用硬解码,需要先做是否支持硬解码检测,接口如下:

/*
检查是否支持H264硬解码
如果支持的话返回NT_ERC_OK
*/NT_UINT32(NT_API *IsSupportH264HardwareDecoder)();


/*
检查是否支持H265硬解码
如果支持的话返回NT_ERC_OK
*/NT_UINT32(NT_API *IsSupportH265HardwareDecoder)();


/*
*设置H264硬解
*is_hardware_decoder: 1:表示硬解, 0:表示不用硬解
*reserve: 保留参数, 当前传0就好
*成功返回NT_ERC_OK
*/NT_UINT32(NT_API *SetH264HardwareDecoder)(NT_HANDLE handle, NT_INT32 is_hardware_decoder, NT_INT32 reserve);


/*
*设置H265硬解
*is_hardware_decoder: 1:表示硬解, 0:表示不用硬解
*reserve: 保留参数, 当前传0就好
*成功返回NT_ERC_OK
*/NT_UINT32(NT_API *SetH265HardwareDecoder)(NT_HANDLE handle, NT_INT32 is_hardware_decoder, NT_INT32 reserve);

5.只解关键帧

移动端,一般对只播放关键帧真正场景,需求不大,但是window端,好多场景下,因为需要播放非常多路,但是又不想占用太多的系统资源,如果全帧播放,路数过多,全部解码、绘制,系统资源占用会加大,如果能灵活的处理,可以随时只播放关键帧,全帧播放切换,对系统性能要求大幅降低,想全帧播放的时候,随时切换全帧绘制。

/*
*设置只解码视频关键帧
*is_only_dec_key_frame: 1:表示只解码关键帧, 0:表示都解码, 默认是0
*成功返回NT_ERC_OK
*/NT_UINT32(NT_API *SetOnlyDecodeVideoKeyFrame)(NT_HANDLE handle, NT_INT32 is_only_dec_key_frame);

6. 缓冲时间设置

缓冲时间,顾名思义,缓存多少数据才开始播放,比如设置2000ms的buffer time,直播模式下,收到2秒数据后,才正常播放。

加大buffer time,会增大播放延迟,好处是,网络抖动的时候,流畅性更好。

/*
设置buffer,最小0ms
*/NT_UINT32(NT_API *SetBuffer)(NT_HANDLE handle, NT_INT32 buffer);

7. 实时静音、实时音量调节

实时静音、实时音量调节顾名思义,播放端可以实时调整播放音量,或者直接静音掉,特别是多路播放场景下,非常有必要。

/*
静音接口,1为静音,0为不静音
*/NT_UINT32(NT_API *SetMute)(NT_HANDLE handle, NT_INT32 is_mute);

/*
设置播放音量, 范围是[0, 100], 0是静音,100是最大音量, 默认是100
调用正确返回NT_ERC_OK
*/NT_UINT32(NT_API *SetAudioVolume)(NT_HANDLE handle, NT_INT32 volume);

8.设置视频画面填充模式

设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view。

相关接口设计如下:

player_api_.SetRenderScaleMode(player_handle_, btn_check_render_scale_mode_.GetCheck() == BST_CHECKED ? 1 : 0);

9.快速启动

快速启动,主要是针对服务器缓存GOP的场景下,快速刷到最新的数据,确保画面的持续性。

/*
设置秒开, 1为秒开, 0为不秒开
*/NT_UINT32(NT_API* SetFastStartup)(NT_HANDLE handle, NT_INT32 isFastStartup);

10. 低延迟模式

低延迟模式下,设置buffer time为0,延迟更低,适用于比如需要操控控制的超低延迟场景下。

/*
设置低延时播放模式,默认是正常播放模式
mode: 1为低延时模式, 0为正常模式,其他只无效
接口调用成功返回NT_ERC_OK
*/NT_UINT32(NT_API* SetLowLatencyMode)(NT_HANDLE handle, NT_INT32 mode);

11. 视频view旋转、水平|垂直翻转

接口主要用于,比如原始的视频倒置等场景下,设备端无法调整时,通过播放端完成图像的正常角度播放。

/*
*上下反转(垂直反转)
*is_flip: 1:表示反转, 0:表示不反转
*/NT_UINT32(NT_API *SetFlipVertical)(NT_HANDLE handle, NT_INT32 is_flip);


/*
*水平反转
*is_flip: 1:表示反转, 0:表示不反转
*/NT_UINT32(NT_API *SetFlipHorizontal)(NT_HANDLE handle, NT_INT32 is_flip);


/*
设置旋转,顺时针旋转
degress: 设置0, 90, 180, 270度有效,其他值无效
注意:除了0度,其他角度播放会耗费更多CPU
接口调用成功返回NT_ERC_OK
*/NT_UINT32(NT_API* SetRotation)(NT_HANDLE handle, NT_INT32 degress);

12. 设置实时回调下载速度

调用实时下载速度接口,通过设置下载速度时间间隔,和是否需要上报当前下载速度,实现APP层和底层SDK更友好的交互。

/*
设置下载速度上报, 默认不上报下载速度
is_report: 上报开关, 1: 表上报. 0: 表示不上报. 其他值无效.
report_interval: 上报时间间隔(上报频率),单位是秒,最小值是1秒1次. 如果小于1且设置了上报,将调用失败
注意:如果设置上报的话,请设置SetEventCallBack, 然后在回调函数里面处理这个事件.
上报事件是:NT_SP_E_EVENT_ID_DOWNLOAD_SPEED
这个接口必须在StartXXX之前调用
成功返回NT_ERC_OK
*/NT_UINT32(NT_API *SetReportDownloadSpeed)(NT_HANDLE handle,
NT_INT32 is_report, NT_INT32 report_interval);


/*
主动获取下载速度
speed: 返回下载速度,单位是Byte/s
(注意:这个接口必须在startXXX之后调用,否则会失败)
成功返回NT_ERC_OK
*/NT_UINT32(NT_API *GetDownloadSpeed)(NT_HANDLE handle, NT_INT32* speed);

13. 实时快照

简单来说,播放过程中,是不是要存取当前的播放画面。

/*
捕获图片
file_name_utf8: 文件名称,utf8编码
call_back_data: 回调时用户自定义数据
call_back: 回调函数,用来通知用户截图已经完成或者失败
成功返回 NT_ERC_OK
只有在播放时调用才可能成功,其他情况下调用,返回错误.
因为生成PNG文件比较耗时,一般需要几百毫秒,为防止CPU过高,SDK会限制截图请求数量,当超过一定数量时,
调用这个接口会返回NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS. 这种情况下, 请延时一段时间,等SDK处理掉一些请求后,再尝试.
*/NT_UINT32(NT_API* CaptureImage)(NT_HANDLE handle, NT_PCSTR file_name_utf8,
NT_PVOID call_back_data, SP_SDKCaptureImageCallBack call_back);

调用实例如下:

voidCSmartPlayerDlg::OnBnClickedButtonCaptureImage(){
  if ( capture_image_path_.empty() )
  {
    AfxMessageBox(_T("请先设置保存截图文件的目录! 点击截图左边的按钮设置!"));
    return;
  }

  if ( player_handle_ == NULL )
  {
    return;
  }

  if ( !is_playing_ )
  {
    return;
  }

  std::wostringstream ss;
  ss << capture_image_path_;

  if ( capture_image_path_.back() != L'\\' )
  {
    ss << L"\\";
  }

  SYSTEMTIME sysTime;
  ::GetLocalTime(&sysTime);

  ss << L"SmartPlayer-"
    << std::setfill(L'0') << std::setw(4) << sysTime.wYear
    << std::setfill(L'0') << std::setw(2) << sysTime.wMonth
    << std::setfill(L'0') << std::setw(2) << sysTime.wDay
    << L"-"
    << std::setfill(L'0') << std::setw(2) << sysTime.wHour
    << std::setfill(L'0') << std::setw(2) << sysTime.wMinute
    << std::setfill(L'0') << std::setw(2) << sysTime.wSecond;

  ss << L"-" << std::setfill(L'0') << std::setw(3) << sysTime.wMilliseconds
    << L".png";

  std::wstring_convert<std::codecvt_utf8<wchar_t> > conv;

  auto val_str = conv.to_bytes(ss.str());

  auto ret = player_api_.CaptureImage(player_handle_, val_str.c_str(), NULL, &SM_SDKCaptureImageHandle);
  if (NT_ERC_OK == ret)
  {
    // 发送截图请求成功
  }
  elseif (NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS == ret)
  {
    // 通知用户延时OutputDebugStringA("Too many capture image requests!!!\r\n");
  }
  else
  {
    // 其他失败
  }
}

14. 扩展录像操作

播放端录像,我们做的非常细化,比如可以只录制音频或者只录制视频,设置录像存储路径,设置单个文件size,如果非AAC数据,可以转AAC后再录像。

/*
* 设置是否录视频,默认的话,如果视频源有视频就录,没有就没得录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
* is_record_video: 1 表示录制视频, 0 表示不录制视频, 默认是1
*/NT_UINT32(NT_API *SetRecorderVideo)(NT_HANDLE handle, NT_INT32 is_record_video);


/*
* 设置是否录音频,默认的话,如果视频源有音频就录,没有就没得录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关
* is_record_audio: 1 表示录制音频, 0 表示不录制音频, 默认是1
*/NT_UINT32(NT_API *SetRecorderAudio)(NT_HANDLE handle, NT_INT32 is_record_audio);


/*
设置本地录像目录, 必须是英文目录,否则会失败
*/NT_UINT32(NT_API *SetRecorderDirectory)(NT_HANDLE handle, NT_PCSTR dir);

/*
设置单个录像文件最大大小, 当超过这个值的时候,将切割成第二个文件
size: 单位是KB(1024Byte), 当前范围是 [5MB-800MB], 超出将被设置到范围内
*/NT_UINT32(NT_API *SetRecorderFileMaxSize)(NT_HANDLE handle, NT_UINT32 size);

/*
设置录像文件名生成规则
*/NT_UINT32(NT_API *SetRecorderFileNameRuler)(NT_HANDLE handle, NT_SP_RecorderFileNameRuler* ruler);


/*
设置录像回调接口
*/NT_UINT32(NT_API *SetRecorderCallBack)(NT_HANDLE handle,
NT_PVOID call_back_data, SP_SDKRecorderCallBack call_back);


/*
设置录像时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
注意: 转码会增加性能消耗
*/NT_UINT32(NT_API *SetRecorderAudioTranscodeAAC)(NT_HANDLE handle, NT_INT32 is_transcode);


/*
启动录像
*/NT_UINT32(NT_API *StartRecorder)(NT_HANDLE handle);

/*
停止录像
*/NT_UINT32(NT_API *StopRecorder)(NT_HANDLE handle);

15. 拉流回调编码后的数据(配合转发模块使用)

拉流回调编码后的数据,主要是为了配合转发模块使用,比如拉取rtsp或rtmp流数据,直接转RTMP推送到RTMP服务。

/*
* 设置拉流时,吐视频数据的回调
*/NT_UINT32(NT_API *SetPullStreamVideoDataCallBack)(NT_HANDLE handle,
NT_PVOID call_back_data, SP_SDKPullStreamVideoDataCallBack call_back);

/*
* 设置拉流时,吐音频数据的回调
*/NT_UINT32(NT_API *SetPullStreamAudioDataCallBack)(NT_HANDLE handle,
NT_PVOID call_back_data, SP_SDKPullStreamAudioDataCallBack call_back);


/*
设置拉流时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
注意: 转码会增加性能消耗
*/NT_UINT32(NT_API *SetPullStreamAudioTranscodeAAC)(NT_HANDLE handle, NT_INT32 is_transcode);


/*
启动拉流
*/NT_UINT32(NT_API *StartPullStream)(NT_HANDLE handle);

/*
停止拉流
*/NT_UINT32(NT_API *StopPullStream)(NT_HANDLE handle);

16. H264用户数据回调或SEI数据回调

如发送端在264编码时,加了自定义的user data数据,可以通过以下接口实现数据回调,如需直接回调SEI数据,调下面SEI回调接口即可。

/*
设置用户数据回调
*/NT_UINT32(NT_API *SetUserDataCallBack)(NT_HANDLE handle,
NT_PVOID call_back_data, NT_SP_SDKUserDataCallBack call_back);

调用实例如下:

extern"C"NT_VOID NT_CALLBACK NT_SP_SDKUserDataHandle(NT_HANDLE handle, NT_PVOID user_data,
  NT_INT32  data_type,
  NT_PVOID  data,
  NT_UINT32 size,
  NT_UINT64 timestamp,
  NT_UINT64 reserve1,
  NT_INT64  reserve2,
  NT_PVOID  reserve3){
  if ( 1 == data_type )
  {
    std::wostringstream oss;
    oss << L"userdata ";

    const NT_BYTE* byte_data = reinterpret_cast<const NT_BYTE*>(data);
    if ( byte_data != nullptr && size > 0 )
    {
      oss << L" byte data size=" << size;
    }

    std::wstring_convert<std::codecvt_utf8<wchar_t> > conv;

    oss << L" t:" << timestamp << L"\r\n";

    OutputDebugStringW(oss.str().c_str());
  }
  elseif ( 2 == data_type )
  {
    const NT_CHAR* str_data = reinterpret_cast<const NT_CHAR*>(data);
    if (str_data != nullptr && size > 0)
    {
      std::unique_ptr<std::string> s(new std::string(str_data, str_data + size));

      // oss << L" utf8 string:" << conv.from_bytes(*s);// oss << L" size=" << size;if ( !s->empty() )
      {
        HWND hwnd = reinterpret_cast<HWND>(user_data);
        if ( hwnd != nullptr && ::IsWindow(hwnd) )
        {
          ::PostMessage(hwnd, WM_USER_SDK_SP_RECV_USER_DATA, (WPARAM)s.release(), (LPARAM)timestamp);
        }
      }
    }
  }

}

17. 设置回调解码后YUV、RGB数据

如需对解码后的yuv或rgb数据,进行二次处理,如人脸识别等,可以通回调yuv rgb接口实现数据二次处理,对于Windows平台来说,如果设备不支持D3D,也可以数据回调上来GDI模式绘制:

player_api_.SetVideoFrameCallBack(player_handle_, NT_SP_E_VIDEO_FRAME_FORMAT_RGB32,
GetSafeHwnd(), SM_SDKVideoFrameHandle);

extern"C"NT_VOID NT_CALLBACK SM_SDKVideoFrameHandle(NT_HANDLE handle, NT_PVOID userData, NT_UINT32 status,
  const NT_SP_VideoFrame* frame){
  /*if (frame != NULL)
  {
  std::ostringstream ss;
  ss << "Receive frame time_stamp:" << frame->timestamp_ << "ms" << "\r\n";
  OutputDebugStringA(ss.str().c_str());
  }*/if ( frame != NULL )
  {
    if ( NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 == frame->format_
      && frame->plane0_ != NULL
      && frame->stride0_ > 0
      && frame->height_ > 0 )
    {
      std::unique_ptr<nt_rgb32_image > pImage(new nt_rgb32_image());

      pImage->size_ = frame->stride0_* frame->height_;
      pImage->data_ = new NT_BYTE[pImage->size_];

      memcpy(pImage->data_, frame->plane0_, pImage->size_);

      pImage->width_  = frame->width_;
      pImage->height_ = frame->height_;
      pImage->stride_ = frame->stride0_;

      HWND hwnd = (HWND)userData;
      if ( hwnd != NULL && ::IsWindow(hwnd) )
      {
        ::PostMessage(hwnd, WM_USER_SDK_RGB32_IMAGE, (WPARAM)handle, (LPARAM)pImage.release());
      }
    }
  }
}

总结

以上就是我们在开发RTMP播放器的一些心得,除了上述基础设计,其他还有些,比如如果系统不支持D3D,需要采用GDI模式绘制,播放界面叠加实时文字,播放画面全屏等,这里就不再赘述。

除Windows平台外,我们还同步开发了Linux、Android、iOS平台的RTMP播放器,大多常规接口四个平台基本统一,延迟也都做到了毫秒级。对于大多数开发者来说,不一定需要实现上述所有部分,只要按照产品诉求,实现其中的30-40%就足够满足特定场景使用了。

一个好的播放器,特别是要满足低延迟稳定的播放(毫秒级延迟),需要注意的点远不止如此,厚积薄发,登上山顶,不是为了饱览风光,是为了寻找更高的山峰!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/862375.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据传输的定义与大数据传输解决方案的选择

当我们需要处理大量的数据时&#xff0c;我们就要把数据从一个地方移动到另一个地方。这个过程就叫做大数据传输。它通常需要用到高速的网络连接、分散的存储系统和数据传输协议&#xff0c;以保证数据的快速、可靠和安全的移动。常用的大数据传输技术有Hadoop分布式文件系统&a…

servlet三大类HttpSevlet,HttpServletRequest,HttpServletResponse介绍

一、HttpServlet HttpServlet类是一个被继承的方法&#xff0c;可以看做一个专门用来响应http请求的类&#xff0c;这个类的所有方法都是为响应http请求服务的&#xff0c;要对一个某个路径谁知http响应时&#xff0c;需要写一个类来继承HttpServlet类&#xff0c;并重写里面的…

BGP基础建邻+宣告实验

实验要求及拓扑 一、实验思路 1.编写静态路由使R1、R2之间可通和使R4、R5之间可通。 2.使用OSPF使R2、R3、R4之间可通。 3.各自宣告AS区域&#xff0c;中间区域两两之间建邻。 4.注意建邻所使用的端口&#xff0c;外部BGP邻居关系和内部BGP邻居关系的区别。 二、上虚拟机操…

企业微信web登录(扫二维码登录)

记录一下企业微信web扫码登录的使用过程。 按惯例&#xff0c;先看登录流程&#xff1a; 步骤 首先&#xff0c; 企业微信后台开启“企业微信授权登陆功能”&#xff0c;“设置授权回调域名” ,授权回调域名必须与访问链接的域名完全一致。&#xff08;访问链接的域名就是扫码…

【Kubernetes】Kubernetes的调度

K8S调度 一、Kubernetes 调度1. Pod 调度介绍2. Pod 启动创建过程3. Kubernetes 的调度过程3.1 调度需要考虑的问题3.2 具体调度过程 二、影响kubernetes调度的因素1. nodeName2. nodeSelector3. 亲和性3.1 三种亲和性的区别3.2 键值运算关系3.3 节点亲和性3.4 Pod 亲和性3.5 P…

高忆管理:创业板股票涨跌幅?

创业板股票涨跌幅限制大于主板商场&#xff0c;为何呈现这样的现象&#xff1f;从多个角度剖析&#xff0c;其中包含方针因素、商场走势、职业危险等多个方面。 首要&#xff0c;方针因素是导致股票涨跌幅波动的一个重要因素。在新的方针环境下&#xff0c;相关部门关于创业板股…

ModaHub魔搭社区——Milvus Cloud向量数据库

向量数据库:在AI时代的快速发展与应用 摘要: 随着人工智能技术的不断进步,向量数据库在处理大规模数据方面发挥着越来越重要的作用。本文介绍了向量数据库的基本概念、应用场景和技术挑战,并详细阐述了Milvus Cloud作为典型的向量数据库产品的技术特点、性能优化和应用案例…

拼多多秋招 考试内容详解和备考技巧

拼多多秋招内容简介 作为线上销售行业的知名企业之一&#xff0c;拼多多的销售模式也得到了越来越多的人认可&#xff0c;而伴随着企业规模的不断扩大&#xff0c;拼多多也需要能力杰出、认可自己公司文化的新员工&#xff0c;从目前的招聘情况来看&#xff0c;拼多多的岗位需…

拿下美团校招:MySQL InnoDB非聚簇索引知识点解析!

大家好&#xff0c;我是你们的小米&#xff0c;在这里欢迎大家来到《小米的技术小屋》&#xff01;今天&#xff0c;我将和大家一起来揭开一个有趣且有深度的话题&#xff0c;那就是来自美团校招面试的一道问题&#xff1a;“MySQL中的InnoDB在什么情况下使用非聚簇索引&#x…

SpringBoot禁用Swagger3

Swagger3默认是启用的&#xff0c;即引入包就启用。 <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> </dependency> <dependency><groupId…

纤维素衍生物辅料行业分析-市场规模达15.67亿美元

纤维素衍生物辅料行业分析&#xff1a;2022年全球纤维素合成生物辅料市场规模达15.67亿美元 关注医药行业的纤维素衍生物辅料。药用辅料是生产药品和调配处方时所用的赋形剂和附加剂&#xff0c;是药物制剂的重要组成部分。纤维素衍生物作为天然高分子衍生材料&#xff0c;具有…

Uniapp使用腾讯地图并进行标点创建和设置保姆教程

使用Uniapp内置地图 首先我们需要创建一个uniapp项目 首先我们需要创建一个uniapp项目 我们在HBuilder左上角点击文件新建创建一个项目 然后下面这张图的话就是uniapp创建项目过程当中需要注意的一些点和具体的操作 然后我们创建完项目之后进入到项目pages文件夹下&#xff…

面试热题(二叉树的锯齿形层次遍历)

给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3…

Spring @Profile注解使用和源码解析

使用 带有Profile的注解的bean的不会被注册进IOC容器&#xff0c;需要为其设置环境变量激活&#xff0c;才能注册进IOC容器&#xff0c;如下通过setActiveProfiles设置了dev值&#xff0c;那么这三个值所对应的Bean会被注册进IOC容器。当然&#xff0c;我们在实际使用中&#…

Grafana技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》-附带监控服务器

阿丹&#xff1a; Prometheus技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》_一单成的博客-CSDN博客 在正确安装了Prometheus之后开始使用并安装Grafana作为Prometheus的仪表盘。 一、拉取镜像 搜索可拉取版本 docker search Grafana拉取镜像 docker pull gra…

cmake (更新中)

概述 关于 CMake CMake 是一个可扩展的开源系统&#xff0c;以一种与操作系统和编译器无关的方式来管理构建过程。与许多跨平台系统不同&#xff0c;CMake 被设计为与本机构建环境配合使用。在每个源代码目录中放置简单的配置文件&#xff08;称为 CMakeLists.txt 文件&#xf…

VLOOKUP函数使用

在Excel中&#xff0c;VLOOKUP函数用于在一个范围内查找某个值&#xff0c;并返回该值所在行的指定列的内容。VLOOKUP函数的基本语法如下&#xff1a; VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])参数说明&#xff1a; lookup_value&#xff1a;要查…

MySQL_事务学习笔记

事务 注意&#xff1a;一定要使用 Innodb 存储引擎 概述&#xff1a;一组操作的集合&#xff0c;是不可分割的工作单元&#xff0c;会把一个部分当成一个整体来处理&#xff0c;事务会把操作同时提交或者是撤销。要么同时成功&#xff0c;要么同时失败。 比如&#xff1a;上云…

汽车制造业上下游协作时 外发数据如何防泄露?

数据文件是制造业企业的核心竞争力&#xff0c;一旦发生数据外泄&#xff0c;就会给企业造成经济损失&#xff0c;严重的&#xff0c;可能会带来知识产权剽窃损害、名誉伤害等。汽车制造业&#xff0c;会涉及到重要的汽车设计图纸&#xff0c;像小米发送汽车设计图纸外泄事件并…

离线安装vscode插件,导出 Visual Studio Code 的扩展应用,并离线安装

在没有网络的情况下&#xff0c;如何安装vscode插件 1.使用之前电脑安装过的插件包 Visual Studio Code 的扩展应用安装位置在文件夹 .vscode/extensions 下。不同平台&#xff0c;它位于&#xff1a; Windows %USERPROFILE%\.vscode\extensions Mac ~/.vscode/extensions L…