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

本文涉及的产品
视觉智能开放平台,视频资源包5000点
视觉智能开放平台,分割抠图1万点
视觉智能开放平台,图像资源包5000点
简介: 对话音视频牛哥:如何设计功能齐全的跨平台低延迟RTMP播放器

开发背景

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

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

整体方案架构

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

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


视沃科技产品架构图.png

首版设计目标

  • 自有框架,易于扩展;
  • 支持各种异常网络状态处理,如断网重连等;
  • 有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为例,大概介绍下常用的接口设计。

windows播放器-正常角度.png

1. Init/UnInit()接口

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

/*flag目前传0,后面扩展用, pReserve传NULL,扩展用,成功返回 NT_ERC_OK*/NT_UINT32(NT_API*Init)(NT_UINT32flag, NT_PVOIDpReserve);
/*这个是最后一个调用的接口成功返回 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_PHANDLEpHandle, NT_HWNDhwnd, NT_UINT32flag, NT_PVOIDpReserve);
/*调用这个接口之后handle失效,成功返回 NT_ERC_OK*/NT_UINT32(NT_API*Close)(NT_HANDLEhandle);

3. 网络状态回调

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

/*设置事件回调,如果想监听事件的话,建议调用Open成功后,就调用这个接口*/NT_UINT32(NT_API*SetEventCallBack)(NT_HANDLEhandle,
NT_PVOIDcall_back_data, NT_SP_SDKEventCallBackcall_back);

demo实现实例:

LRESULTCSmartPlayerDlg::OnSDKEvent(WPARAMwParam, LPARAMlParam){
if (!is_playing_&&!is_recording_)
    {
returnS_OK;
    }
NT_UINT32event_id= (NT_UINT32)(wParam);
if ( NT_SP_E_EVENT_ID_PLAYBACK_REACH_EOS==event_id )
    {
StopPlayback();
returnS_OK;
    }
elseif ( NT_SP_E_EVENT_ID_RECORDER_REACH_EOS==event_id )
    {
StopRecorder();
returnS_OK;
    }
elseif ( NT_SP_E_EVENT_ID_RTSP_STATUS_CODE==event_id )
    {
intstatus_code= (int)lParam;
if ( 401==status_code )
        {
HandleVerification();
        }
returnS_OK;
    }
elseif (NT_SP_E_EVENT_ID_NEED_KEY==event_id)
    {
HandleKeyEvent(false);
returnS_OK;
    }
elseif (NT_SP_E_EVENT_ID_KEY_ERROR==event_id)
    {
HandleKeyEvent(true);
returnS_OK;
    }
elseif ( NT_SP_E_EVENT_ID_PULLSTREAM_REACH_EOS==event_id )
    {
if (player_handle_!=NULL)
        {
player_api_.StopPullStream(player_handle_);
        }
returnS_OK;
    }
elseif ( NT_SP_E_EVENT_ID_DURATION==event_id )
    {
NT_INT64duration= (NT_INT64)(lParam);
edit_duration_.SetWindowTextW(GetHMSMsFormatStr(duration, false, false).c_str());
returnS_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::wostringstreamss;
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());*/    }
CStringshow_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::wostringstreamss;
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::wostringstreamss;
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);
returnS_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_HANDLEhandle, NT_INT32is_hardware_decoder, NT_INT32reserve);
/**设置H265硬解*is_hardware_decoder: 1:表示硬解, 0:表示不用硬解*reserve: 保留参数, 当前传0就好*成功返回NT_ERC_OK*/NT_UINT32(NT_API*SetH265HardwareDecoder)(NT_HANDLEhandle, NT_INT32is_hardware_decoder, NT_INT32reserve);

5.只解关键帧

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

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

6. 缓冲时间设置

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

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

/*设置buffer,最小0ms*/NT_UINT32(NT_API*SetBuffer)(NT_HANDLEhandle, NT_INT32buffer);

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

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

/*静音接口,1为静音,0为不静音*/NT_UINT32(NT_API*SetMute)(NT_HANDLEhandle, NT_INT32is_mute);
/*设置播放音量, 范围是[0, 100], 0是静音,100是最大音量, 默认是100调用正确返回NT_ERC_OK*/NT_UINT32(NT_API*SetAudioVolume)(NT_HANDLEhandle, NT_INT32volume);

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_HANDLEhandle, NT_INT32isFastStartup);

10. 低延迟模式

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

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

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

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

/**上下反转(垂直反转)*is_flip: 1:表示反转, 0:表示不反转*/NT_UINT32(NT_API*SetFlipVertical)(NT_HANDLEhandle, NT_INT32is_flip);
/**水平反转*is_flip: 1:表示反转, 0:表示不反转*/NT_UINT32(NT_API*SetFlipHorizontal)(NT_HANDLEhandle, NT_INT32is_flip);
/*设置旋转,顺时针旋转degress: 设置0, 90, 180, 270度有效,其他值无效注意:除了0度,其他角度播放会耗费更多CPU接口调用成功返回NT_ERC_OK*/NT_UINT32(NT_API*SetRotation)(NT_HANDLEhandle, NT_INT32degress);

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_HANDLEhandle,
NT_INT32is_report, NT_INT32report_interval);
/*主动获取下载速度speed: 返回下载速度,单位是Byte/s(注意:这个接口必须在startXXX之后调用,否则会失败)成功返回NT_ERC_OK*/NT_UINT32(NT_API*GetDownloadSpeed)(NT_HANDLEhandle, 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_HANDLEhandle, NT_PCSTRfile_name_utf8,
NT_PVOIDcall_back_data, SP_SDKCaptureImageCallBackcall_back);

调用实例如下:

voidCSmartPlayerDlg::OnBnClickedButtonCaptureImage(){
if ( capture_image_path_.empty() )
  {
AfxMessageBox(_T("请先设置保存截图文件的目录! 点击截图左边的按钮设置!"));
return;
  }
if ( player_handle_==NULL )
  {
return;
  }
if ( !is_playing_ )
  {
return;
  }
std::wostringstreamss;
ss<<capture_image_path_;
if ( capture_image_path_.back() !=L'\\' )
  {
ss<<L"\\";
  }
SYSTEMTIMEsysTime;
  ::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;
autoval_str=conv.to_bytes(ss.str());
autoret=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_HANDLEhandle, NT_INT32is_record_video);
/** 设置是否录音频,默认的话,如果视频源有音频就录,没有就没得录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关* is_record_audio: 1 表示录制音频, 0 表示不录制音频, 默认是1*/NT_UINT32(NT_API*SetRecorderAudio)(NT_HANDLEhandle, NT_INT32is_record_audio);
/*设置本地录像目录, 必须是英文目录,否则会失败*/NT_UINT32(NT_API*SetRecorderDirectory)(NT_HANDLEhandle, NT_PCSTRdir);
/*设置单个录像文件最大大小, 当超过这个值的时候,将切割成第二个文件size: 单位是KB(1024Byte), 当前范围是 [5MB-800MB], 超出将被设置到范围内*/NT_UINT32(NT_API*SetRecorderFileMaxSize)(NT_HANDLEhandle, NT_UINT32size);
/*设置录像文件名生成规则*/NT_UINT32(NT_API*SetRecorderFileNameRuler)(NT_HANDLEhandle, NT_SP_RecorderFileNameRuler*ruler);
/*设置录像回调接口*/NT_UINT32(NT_API*SetRecorderCallBack)(NT_HANDLEhandle,
NT_PVOIDcall_back_data, SP_SDKRecorderCallBackcall_back);
/*设置录像时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.注意: 转码会增加性能消耗*/NT_UINT32(NT_API*SetRecorderAudioTranscodeAAC)(NT_HANDLEhandle, NT_INT32is_transcode);
/*启动录像*/NT_UINT32(NT_API*StartRecorder)(NT_HANDLEhandle);
/*停止录像*/NT_UINT32(NT_API*StopRecorder)(NT_HANDLEhandle);

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

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

/** 设置拉流时,吐视频数据的回调*/NT_UINT32(NT_API*SetPullStreamVideoDataCallBack)(NT_HANDLEhandle,
NT_PVOIDcall_back_data, SP_SDKPullStreamVideoDataCallBackcall_back);
/** 设置拉流时,吐音频数据的回调*/NT_UINT32(NT_API*SetPullStreamAudioDataCallBack)(NT_HANDLEhandle,
NT_PVOIDcall_back_data, SP_SDKPullStreamAudioDataCallBackcall_back);
/*设置拉流时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.注意: 转码会增加性能消耗*/NT_UINT32(NT_API*SetPullStreamAudioTranscodeAAC)(NT_HANDLEhandle, NT_INT32is_transcode);
/*启动拉流*/NT_UINT32(NT_API*StartPullStream)(NT_HANDLEhandle);
/*停止拉流*/NT_UINT32(NT_API*StopPullStream)(NT_HANDLEhandle);

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

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

/*设置用户数据回调*/NT_UINT32(NT_API*SetUserDataCallBack)(NT_HANDLEhandle,
NT_PVOIDcall_back_data, NT_SP_SDKUserDataCallBackcall_back);

调用实例如下:

extern"C"NT_VOIDNT_CALLBACKNT_SP_SDKUserDataHandle(NT_HANDLEhandle, NT_PVOIDuser_data,
NT_INT32data_type,
NT_PVOIDdata,
NT_UINT32size,
NT_UINT64timestamp,
NT_UINT64reserve1,
NT_INT64reserve2,
NT_PVOIDreserve3){
if ( 1==data_type )
  {
std::wostringstreamoss;
oss<<L"userdata ";
constNT_BYTE*byte_data=reinterpret_cast<constNT_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 )
  {
constNT_CHAR*str_data=reinterpret_cast<constNT_CHAR*>(data);
if (str_data!=nullptr&&size>0)
    {
std::unique_ptr<std::string>s(newstd::string(str_data, str_data+size));
// oss << L" utf8 string:" << conv.from_bytes(*s);// oss << L" size=" << size;if ( !s->empty() )      {
HWNDhwnd=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_VOIDNT_CALLBACKSM_SDKVideoFrameHandle(NT_HANDLEhandle, NT_PVOIDuserData, NT_UINT32status,
constNT_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(newnt_rgb32_image());
pImage->size_=frame->stride0_*frame->height_;
pImage->data_=newNT_BYTE[pImage->size_];
memcpy(pImage->data_, frame->plane0_, pImage->size_);
pImage->width_=frame->width_;
pImage->height_=frame->height_;
pImage->stride_=frame->stride0_;
HWNDhwnd= (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%就足够满足特定场景使用了。

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

相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
相关文章
|
Web App开发 数据采集 物联网
Android平台基于RTMP或RTSP的一对一音视频互动技术方案探讨
随着智能门禁等物联网产品的普及,越来越多的开发者对音视频互动体验提出了更高的要求。目前市面上大多一对一互动都是基于WebRTC,优点不再赘述,我们这里先说说可能需要面临的问题:WebRTC的服务器部署非常复杂,可以私有部署,但是非常复杂。传输基于UDP,很难保证传输质量,由于UDP是不可靠的传输协议,在复杂的公网网络环境下,各种突发流量、偶尔的传输错误、网络抖动、超时等等都会引起丢包异常,都会在一定程度上影响音视频通信的质量,难以应对复杂的互联网环境,如跨区跨运营商、低带宽、高丢包等场景,行话说的好:从demo到实用,中间还差1万个WebRTC。
154 0
|
1月前
|
编解码 Linux API
从FFplay到自定义播放器:构建高性能多媒体应用程序的进阶之路
【10月更文挑战第15天】多媒体应用程序的开发是一个复杂的过程,尤其是在追求高性能和定制化体验时。本文将引导你从使用FFplay作为起点,逐步过渡到构建一个完全自定义的播放器。我们将探讨FFmpeg库的高级用法、多媒体同步原理、跨平台开发注意事项,以及如何实现用户界面与音视频解码的无缝集成。
37 1
|
2月前
|
Linux Android开发 iOS开发
Windows平台RTSP|RTMP播放器如何实现实时录像功能
Windows平台RTSP、RTMP播放器实时录像接口设计,实际上,除了Windows平台,我们Linux、Android、iOS平台也是一样的设计,单纯的录像模块,如果做的全面,也不是一两个接口可以搞定的
|
2月前
|
编解码 Dart 网络协议
Flutter如何玩转超低延迟RTSP/RTMP播放,跨平台视频流体验大升级,让你的应用秒变直播神器!
【9月更文挑战第3天】Flutter作为谷歌推出的跨平台移动UI框架,凭借高性能和丰富的生态系统广受好评。本文详细介绍如何在Flutter应用中实现低延迟的跨平台RTSP/RTMP播放,并提供具体示例代码。首先介绍了如何使用`flutter_vlc_player`播放RTSP流,然后讨论了优化视频播放以降低延迟的方法,包括调整播放器配置等。通过选用合适的播放器插件并进行优化,Flutter可在视频流播放领域提供卓越的用户体验。随着生态的发展,Flutter有望成为视频流媒体开发的首选框架。
330 6
|
2月前
|
Web App开发 网络协议 Android开发
Android平台一对一音视频通话方案大比拼:WebRTC VS RTMP VS RTSP,谁才是王者?
【9月更文挑战第4天】本文详细对比了在Android平台上实现一对一音视频通话时常用的WebRTC、RTMP及RTSP三种技术方案。从技术原理、性能表现与开发难度等方面进行了深入分析,并提供了示例代码。WebRTC适合追求低延迟和高质量的场景,但开发成本较高;RTMP和RTSP则在简化开发流程的同时仍能保持较好的传输效果,适用于不同需求的应用场景。
172 1
|
3月前
|
编解码 vr&ar 图形学
惊世骇俗!Unity下如何实现低至毫秒级的全景RTMP|RTSP流渲染,颠覆你的视觉体验!
【8月更文挑战第14天】随着虚拟现实技术的进步,全景视频作为一种新兴媒体形式,在Unity中实现低延迟的RTMP/RTSP流渲染变得至关重要。这不仅能够改善用户体验,还能广泛应用于远程教育、虚拟旅游等实时交互场景。本文介绍如何在Unity中实现全景视频流的低延迟渲染,并提供代码示例。首先确保Unity开发环境及所需插件已就绪,然后利用`unity-rtsp-rtmp-client`插件初始化客户端并设置回调。通过FFmpeg等工具解码视频数据并更新至全景纹理,同时采用硬件加速、调整缓冲区大小等策略进一步降低延迟。此方案需考虑网络状况与异常处理,确保应用程序的稳定性和可靠性。
69 1
|
3月前
|
数据采集 编解码 开发工具
Android平台实现无纸化同屏并推送RTMP或轻量级RTSP服务(毫秒级延迟)
一个好的无纸化同屏系统,需要考虑的有整体组网、分辨率、码率、实时延迟、音视频同步和连续性等各个指标,做容易,做好难
|
3月前
|
编解码 开发工具 数据安全/隐私保护
如何快速实现Windows平台屏幕摄像头采集并推送RTMP|轻量级RTSP服务能力?
一个好的推送模块,除了实现高效率的编码传输外,还要有好的音视频采集机制和灵活的架构支持,便于后期功能扩展,比如实时快照、预览、实时录像等。除此之外,还要有好的交互机制(比如envent callback)、低延迟和长期运行稳定的性能。
|
3月前
|
编解码 Dart 网络协议
"震撼揭秘!Flutter如何玩转超低延迟RTSP/RTMP播放,跨平台视频流体验大升级,让你的应用秒变直播神器!"
【8月更文挑战第15天】Flutter作为跨平台UI框架,以其高效性和丰富生态著称。本文详述如何利用flutter_vlc_player等插件在Flutter中实现低延迟RTSP/RTMP播放,并提供代码示例。通过优化播放器设置,如禁用缓冲、启用帧丢弃等,可进一步减少延迟,提升用户观看体验,展现了Flutter在视频流媒体应用中的强大潜力。
92 0
|
6月前
|
Web App开发 安全 API
想开发一款带有视频通话/共享屏幕功能的产品?那WebRTC是你必须要知道的!
一名技术爱好者在研究如何为开源项目集成视频通话功能时,深入学习了WebRTC技术。WebRTC是一个API,允许浏览器和应用实现实时音视频通信,简化了之前复杂的技术挑战,如音视频处理和网络传输。该技术可用于视频通话、桌面共享、视频会议等多种场景。在WebRTC中,通过信令交换、STUN/TURN服务器和ICE框架处理网络连接和通信路径,实现点对点连接。与WebSocket不同,WebRTC专注于高质量实时通信,使用UDP协议以降低延迟。接下来的文章将分享如何实现WebRTC的视频通话功能。
下一篇
无影云桌面