大牛直播SDK:如何設計一款跨平臺低延遲的RTMP/RTSP直播播放器

開發背景

2015年,當我們試圖在市面上找一款專供直播播放使用的低延遲播放器,來配合測試我們的RTMP推送模塊使用時,居然發現沒有一款好用的,市面上的,如VLC或Vitamio,說白了都是基于FFMPEG,在點播這塊支持格式很多,也非常優異,但是直播這塊,特別是RTMP,延遲要幾秒鐘,對如純音頻、純視頻播放,快速啟播、網絡異常狀態處理、集成復雜度等各方面,支持非常差,而且因為功能強大,bug很多,除了行業內資深的開發者能駕馭,好多開發者甚至連編譯整體環境,都要耗費很大的精力。

我們的直播播放器,始于Windows平臺,Android和iOS同步開發,基于上述開源播放器的各種缺點,我們考慮全自研框架,確保整體設計跨平臺,再保障播放流程度的前提下,盡可能的做到毫秒級延遲,接口設計三個平臺統一化,確保多平臺集成復雜度降到最低。

整體方案架構

RTMP或RTSP直播播放器,目標很明確,從RTMP服務器(自建服務器或CDN)或RTSP服務器(或NVR/IPC/編碼器等)拉取流數據,完成數據解析、解碼、音視頻數據同步、繪制。

具體對應下圖“接收端”部分:

?

初期模塊設計目標

  • 自有框架,易于擴展,自適應算法讓延遲更低、解碼繪制效率更高;
  • 支持各種異常網絡狀態處理,如斷網重連、網絡抖動等控制;
  • 有Event狀態回調,確保開發者可以了解到播放端整體的狀態,從純黑盒不可控,到更智能的了解到整體播放狀態;
  • 支持多實例播放;
  • 視頻支持H.264,音頻支持AAC/PCMA/PCMU;
  • 支持緩沖時間設置(buffer time);
  • 實時靜音。

經過迭代后的功能

  • [支持播放協議]RTSP、RTMP,毫秒級延遲;
  • ?[多實例播放]支持多實例播放;
  • ?[事件回調]支持網絡狀態、buffer狀態等回調;
  • ?[音視頻加密]Windows平臺支持RTMP推送端加密(AES/SM4(國密))音視頻數據正常播放
  • ?[視頻格式]支持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支持設置Surface模式硬解和普通模式硬解碼;
  • ?[緩沖時間設置]支持buffer time設置;
  • ?[首屏秒開]支持首屏秒開模式;
  • ?[低延遲模式]支持類似于線上娃娃機等直播方案的超低延遲模式設置(公網200~400ms);
  • ?[復雜網絡處理]支持斷網重連等各種網絡環境自動適配;
  • ?[快速切換URL]支持播放過程中,快速切換其他URL,內容切換更快;
  • ?[音視頻多種render機制]Android平臺,視頻:surfaceview/OpenGL ES,音頻:AudioTrack/OpenSL ES;
  • ?[實時靜音]支持播放過程中,實時靜音/取消靜音;
  • ?[實時快照]支持播放過程中截取當前播放畫面;
  • ?[只播關鍵幀]Windows平臺支持實時設置是否只播放關鍵幀;
  • ?[渲染角度]支持0°,90°,180°和270°四個視頻畫面渲染角度設置;
  • ?[渲染鏡像]支持水平反轉、垂直反轉模式設置;
  • ?[實時下載速度更新]支持當前下載速度實時回調(支持設置回調時間間隔);
  • ?[ARGB疊加]Windows平臺支持ARGB圖像疊加到顯示視頻(參看C++的DEMO);
  • ?[解碼前視頻數據回調]支持H.264/H.265數據回調;
  • ?[解碼后視頻數據回調]支持解碼后YUV/RGB數據回調;
  • ?[解碼后視頻數據縮放回調]Windows平臺支持指定回調圖像大小的接口(可以對原視圖像縮放后再回調到上層);
  • ?[解碼前音頻數據回調]支持AAC/PCMA/PCMU/SPEEX數據回調;
  • ?[音視頻自適應]支持播放過程中,音視頻信息改變后自適應;
  • ?[擴展錄像功能]支持RTSP/RTMP H.264、擴展H.265流錄制,支持PCMA/PCMU/Speex轉AAC后錄制,支持設置只錄制音頻或視頻等;

RTMP、RTSP直播播放開發設計考慮的點

1. 低延遲:大多數RTSP的播放都面向直播場景,所以,如果延遲過大,嚴重影響體驗,所以,低延遲是衡量一個好的RTSP播放器非常重要的指標,目前大牛直播SDK的RTSP直播播放延遲比開源播放器更優異,而且長時間運行下,不會造成延遲累積;

2. 音視頻同步處理有些播放器為了追求低延遲,甚至不做音視頻同步,拿到audio video直接播放,導致a/v不同步,還有就是時間戳亂跳等各種問題,大牛直播SDK提供的播放器,具備好的時間戳同步和異常時間戳矯正機制;

3. 支持多實例:大牛直播SDK提供的播放器支持同時播放多路音視頻數據,比如4-8-9窗口,大多開源播放器對多實例支持不太友好;

4. 支持buffer time設置:在一些有網絡抖動的場景,播放器需要支持buffer time設置,一般來說,以毫秒計,開源播放器對此支持不夠友好;

5. TCP/UDP模式設定自動切換:考慮到好多服務器僅支持TCP或UDP模式,一個好的RTSP播放器需要支持TCP/UDP模式設置,如鏈接不支持TCP或UDP,大牛直播SDK可自動切換,,開源播放器不具備自動切換TCP/UDP能力;

6. 實時靜音:比如,多窗口播放RTSP流,如果每個audio都播放出來,體驗非常不好,所以實時靜音功能非常必要,開源播放器不具備實時靜音功能;

7. 視頻view旋轉:好多攝像頭由于安裝限制,導致圖像倒置,所以一個好的RTSP播放器應該支持如視頻view實時旋轉(0° 90° 180° 270°)、水平反轉、垂直反轉,開源播放器不具備此功能;

8. 支持解碼后audio/video數據輸出:大牛直播SDK接觸到好多開發者,希望能在播放的同時,獲取到YUV或RGB數據,進行人臉匹配等算法分析,開源播放器不具備此功能;

9. 實時快照:感興趣或重要的畫面,實時截取下來非常必要,一般播放器不具備快照能力,開源播放器不具備此功能;

10. 網絡抖動處理(如斷網重連):穩定的網絡處理機制、支持如斷網重連等,開源播放器對網絡異常處理支持較差;

11. 長期運行穩定性:不同于市面上的開源播放器,大牛直播SDK提供的Windows平臺RTSP直播播放SDK適用于數天長時間運行,開源播放器對長時間運行穩定性支持較差;

12. log信息記錄:整體流程機制記錄到LOG文件,確保出問題時,有據可依,開源播放器幾無log記錄。

13. 實時下載速度反饋:大牛直播SDK提供音視頻流實時下載回調,并可設置回調時間間隔,確保實時下載速度反饋,以此來監聽網絡狀態,開源播放器不具備此能力;

14. 異常狀態處理Event狀態回調如播放的過程中,斷網、網絡抖動、等各種場景,大牛直播SDK提供的播放器可實時回調相關狀態,確保上層模塊感知處理,開源播放器對此支持不好;

15. 關鍵幀/全幀播放實時切換:特別是播放多路畫面的時候,如果路數過多,全部解碼、繪制,系統資源占用會加大,如果能靈活的處理,可以隨時只播放關鍵幀,全幀播放切換,對系統性能要求大幅降低。

接口設計

好多開發者,在初期設計接口的時候,如果沒有足夠的音視頻背景,很容易反復推翻之前的設計,我們以Windows平臺為例,共享我們的設計思路,如需要下載demo工程源碼,可以到 GitHub 下載參考:

smart_player_sdk.h

#ifdef __cplusplus
extern "C"{
#endif

	typedef struct _SmartPlayerSDKAPI
	{
		/*
		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)();

		/*
		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);


		/*
		設置事件回調,如果想監聽事件的話,建議調用Open成功后,就調用這個接口
		*/
		NT_UINT32(NT_API *SetEventCallBack)(NT_HANDLE handle,
			NT_PVOID call_back_data, NT_SP_SDKEventCallBack call_back);

		/*
		設置視頻大小回調接口
		*/
		NT_UINT32(NT_API *SetVideoSizeCallBack)(NT_HANDLE handle, 
			NT_PVOID call_back_data, SP_SDKVideoSizeCallBack call_back);


		/*
		設置視頻回調, 吐視頻數據出來
		frame_format: 只能是NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, NT_SP_E_VIDEO_FRAME_FROMAT_I420
		*/
		NT_UINT32(NT_API *SetVideoFrameCallBack)(NT_HANDLE handle,
			NT_INT32 frame_format,
			NT_PVOID call_back_data, SP_SDKVideoFrameCallBack call_back);


		/*
		設置視頻回調, 吐視頻數據出來, 可以指定吐出來的視頻寬高
		*handle: 播放句柄
		*scale_width:縮放寬度(必須是偶數,建議是 16 的倍數)
		*scale_height:縮放高度(必須是偶數
		*scale_filter_mode: 縮放質量, 0 的話 SDK 將使用默認值, 目前可設置范圍為[1, 3], 值越大 縮放質量越好,但越耗性能
		*frame_format: 只能是NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, NT_SP_E_VIDEO_FRAME_FROMAT_I420
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetVideoFrameCallBackV2)(NT_HANDLE handle,
			NT_INT32 scale_width, NT_INT32 scale_height,
			NT_INT32 scale_filter_mode, NT_INT32 frame_format,
			NT_PVOID call_back_data, SP_SDKVideoFrameCallBack call_back);

		/*
		*設置繪制視頻幀時,視頻幀時間戳回調
		*注意如果當前播放流是純音頻,那么將不會回調,這個僅在有視頻的情況下才有效
		*/
		NT_UINT32(NT_API *SetRenderVideoFrameTimestampCallBack)(NT_HANDLE handle,
			NT_PVOID call_back_data, SP_SDKRenderVideoFrameTimestampCallBack call_back);


		/*
		設置音頻PCM幀回調, 吐PCM數據出來,目前每幀大小是10ms.
		*/
		NT_UINT32(NT_API *SetAudioPCMFrameCallBack)(NT_HANDLE handle,
			NT_PVOID call_back_data, NT_SP_SDKAudioPCMFrameCallBack call_back);


		/*
		設置用戶數據回調
		*/
		NT_UINT32(NT_API *SetUserDataCallBack)(NT_HANDLE handle,
			NT_PVOID call_back_data, NT_SP_SDKUserDataCallBack call_back);


		/*
		設置視頻sei數據回調
		*/
		NT_UINT32(NT_API *SetSEIDataCallBack)(NT_HANDLE handle,
			NT_PVOID call_back_data, NT_SP_SDKSEIDataCallBack call_back);

			
		/*
		開始播放,傳URL進去
		注意:這個接口目前不再推薦使用,請使用StartPlay. 為方便老客戶升級,暫時保留. 
		*/
		NT_UINT32(NT_API *Start)(NT_HANDLE handle, NT_PCSTR url, 
			NT_PVOID call_back_data, SP_SDKStartPlayCallBack call_back);
		
		/*
		停止播放
		注意: 這個接口目前不再推薦使用,請使用StopPlay. 為方便老客戶升級,暫時保留. 
		*/
		NT_UINT32(NT_API *Stop)(NT_HANDLE handle);

		/*
		*提供一組新接口++
		*/
		
		/*
		*設置URL
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetURL)(NT_HANDLE handle, NT_PCSTR url);

		/*
		*
		* 設置解密key,目前只用來解密rtmp加密流
		* key: 解密密鑰
		* size: 密鑰長度
		* 成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetKey)(NT_HANDLE handle, const NT_BYTE* key, NT_UINT32 size);


		/*
		*
		* 設置解密向量,目前只用來解密rtmp加密流
		* iv:  解密向量
		* size: 向量長度
		* 成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetDecryptionIV)(NT_HANDLE handle, const NT_BYTE* iv, NT_UINT32 size);


		/*
		handle: 播放句柄
		hwnd: 這個要傳入真正用來繪制的窗口句柄
		is_support: 如果支持的話 *is_support 為1, 不支持的話為0
		接口調用成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *IsSupportD3DRender)(NT_HANDLE handle, NT_HWND hwnd, NT_INT32* is_support);


		/*
		設置繪制窗口句柄,如果在調用Open時設置過,那這個接口可以不調用
		如果在調用Open時設置為NULL,那么這里可以設置一個繪制窗口句柄給播放器
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetRenderWindow)(NT_HANDLE handle, NT_HWND hwnd);

		/*
		* 設置是否播放出聲音,這個和靜音接口是有區別的
		* 這個接口的主要目的是為了用戶設置了外部PCM回調接口后,又不想讓SDK播放出聲音時使用
		* is_output_auido_device: 1: 表示允許輸出到音頻設備,默認是1, 0:表示不允許輸出. 其他值接口返回失敗
		* 成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetIsOutputAudioDevice)(NT_HANDLE handle, NT_INT32 is_output_auido_device);


		/*
		*開始播放, 注意StartPlay和Start不能混用,要么使用StartPlay, 要么使用Start.
		* Start和Stop是老接口,不推薦用。請使用StartPlay和StopPlay新接口
		*/
		NT_UINT32(NT_API *StartPlay)(NT_HANDLE handle);

		/*
		*停止播放
		*/
		NT_UINT32(NT_API *StopPlay)(NT_HANDLE handle);


		/*
		* 設置是否錄視頻,默認的話,如果視頻源有視頻就錄,沒有就沒得錄, 但有些場景下可能不想錄制視頻,只想錄音頻,所以增加個開關
		* 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);

		/*
		* 設置拉流時,吐視頻數據的回調
		*/
		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);


		/*
		*提供一組新接口--
		*/
		 
		/*
		繪制窗口大小改變時,必須調用
		*/
		NT_UINT32(NT_API* OnWindowSize)(NT_HANDLE handle, NT_INT32 cx, NT_INT32 cy);

		/*
		萬能接口, 設置參數, 大多數問題, 這些接口都能解決
		*/
		NT_UINT32(NT_API *SetParam)(NT_HANDLE handle, NT_UINT32 id, NT_PVOID pData);

		/*
		萬能接口, 得到參數, 大多數問題,這些接口都能解決
		*/
		NT_UINT32(NT_API *GetParam)(NT_HANDLE handle, NT_UINT32 id, NT_PVOID pData);

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

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

		/*
		設置RTSP TCP 模式, 1為TCP, 0為UDP, 僅RTSP有效
		*/
		NT_UINT32(NT_API* SetRTSPTcpMode)(NT_HANDLE handle, NT_INT32 isUsingTCP);


		/*
		設置RTSP超時時間, timeout單位為秒,必須大于0
		*/
		NT_UINT32 (NT_API* SetRtspTimeout)(NT_HANDLE handle, NT_INT32 timeout);


		/*
		對于RTSP來說,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式. 
		為了方便使用,有些場景下可以開啟自動嘗試切換開關, 打開后如果udp無法播放,sdk會自動嘗試tcp, 如果tcp方式播放不了,sdk會自動嘗試udp.
		is_auto_switch_tcp_udp: 如果設置1的話, sdk將在tcp和udp之間嘗試切換播放,如果設置為0,則不嘗試切換.
		*/
		NT_UINT32 (NT_API* SetRtspAutoSwitchTcpUdp)(NT_HANDLE handle, NT_INT32 is_auto_switch_tcp_udp);


		/*
		設置秒開, 1為秒開, 0為不秒開
		*/
		NT_UINT32(NT_API* SetFastStartup)(NT_HANDLE handle, NT_INT32 isFastStartup);

		/*
		設置低延時播放模式,默認是正常播放模式
		mode: 1為低延時模式, 0為正常模式,其他只無效
		接口調用成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API* SetLowLatencyMode)(NT_HANDLE handle, NT_INT32 mode);


		/*
		檢查是否支持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);


		/*
		*設置只解碼視頻關鍵幀
		*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);


		/*
		*上下反轉(垂直反轉)
		*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);


		/*
		* 在使用D3D繪制的情況下,給繪制窗口上畫一個logo, logo的繪制由視頻幀驅動, 必須傳入argb圖像
		* argb_data: argb圖像數據, 如果傳null的話,將清除之前設置的logo
		* argb_stride: argb圖像每行的步長(一般都是image_width*4)
		* image_width: argb圖像寬度
		* image_height: argb圖像高度
		* left: 繪制位置的左邊x
		* top: 繪制位置的頂部y
		* render_width: 繪制的寬度
		* render_height: 繪制的高度
		*/
		NT_UINT32(NT_API* SetRenderARGBLogo)(NT_HANDLE handle, 
			const NT_BYTE* argb_data, NT_INT32 argb_stride,
			NT_INT32 image_width, NT_INT32 image_height,
			NT_INT32 left, NT_INT32 top,
			NT_INT32 render_width, NT_INT32 render_height
			);


		/*
		設置下載速度上報, 默認不上報下載速度
		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);


		/*
		獲取視頻時長
		對于直播的話,沒有時長,調用結果未定義
		點播的話,如果獲取成功返回NT_ERC_OK, 如果SDK還在解析中,則返回NT_ERC_SP_NEED_RETRY
		*/
		NT_UINT32(NT_API *GetDuration)(NT_HANDLE handle, NT_INT64* duration);


		/*
		獲取當前播放時間戳, 單位是毫秒(ms)
		注意:這個時間戳是視頻源的時間戳,只支持點播. 直播不支持.
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *GetPlaybackPos)(NT_HANDLE handle, NT_INT64* pos);


		/*
		獲取當前拉流時間戳, 單位是毫秒(ms)
		注意:這個時間戳是視頻源的時間戳,只支持點播. 直播不支持.
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *GetPullStreamPos)(NT_HANDLE handle, NT_INT64* pos);

		/*
		設置播放位置,單位是毫秒(ms)
		注意:直播不支持,這個接口用于點播
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SetPos)(NT_HANDLE handle, NT_INT64 pos);


		/*
		暫停播放
		isPause: 1表示暫停, 0表示恢復播放, 其他錯誤
		注意:直播不存在暫停的概念,所以直播不支持,這個接口用于點播
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *Pause)(NT_HANDLE handle, NT_INT32 isPause);


		/*
		切換URL
		url:要切換的url
		switch_pos: 切換到新url以后,設置的播放位置, 默認請填0, 這個只對設置播放位置的點播url有效, 直播url無效
		reserve: 保留參數
		注意: 1. 如果切換的url和正在播放的url相同,sdk則不做任何處理
		調用前置條件: 已經成功調用了 StartPlay, StartRecorder, StartPullStream 三個中的任意一個接口
		成功返回NT_ERC_OK
		*/
		NT_UINT32(NT_API *SwitchURL)(NT_HANDLE handle, NT_PCSTR url, NT_INT64 switch_pos, NT_INT32 reserve);


		/*
		捕獲圖片
		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);


		/*
		* 使用GDI繪制RGB32數據
		* 32位的rgb格式, r, g, b各占8, 另外一個字節保留, 內存字節格式為: bb gg rr xx, 主要是和windows位圖匹配, 在小端模式下,按DWORD類型操作,最高位是xx, 依次是rr, gg, bb
		* 為了保持和windows位圖兼容,步長(image_stride)必須是width_*4
		* handle: 播放器句柄
		* hdc: 繪制dc
		* x_dst: 繪制面左上角x坐標
		* y_dst: 繪制面左上角y坐標
		* dst_width: 要繪制的寬度
		* dst_height: 要繪制的高度
		* x_src: 源圖像x位置
		* y_src: 原圖像y位置
		* rgb32_data: rgb32數據,格式參見前面的注釋說明
		* rgb32_data_size: 數據大小
		* image_width: 圖像實際寬度
		* image_height: 圖像實際高度
		* image_stride: 圖像步長
		*/
		NT_UINT32(NT_API *GDIDrawRGB32)(NT_HANDLE handle, NT_HDC hdc,
			NT_INT32 x_dst, NT_INT32 y_dst,
			NT_INT32 dst_width, NT_INT32 dst_height,
			NT_INT32 x_src, NT_INT32 y_src,
			NT_INT32 src_width, NT_INT32 src_height,
			const NT_BYTE* rgb32_data, NT_UINT32 rgb32_data_size,
			NT_INT32 image_width, NT_INT32 image_height,
			NT_INT32 image_stride);



		/*
		* 使用GDI繪制ARGB數據
		* 內存字節格式為: bb gg rr alpha, 主要是和windows位圖匹配, 在小端模式下,按DWORD類型操作,最高位是alpha, 依次是rr, gg, bb
		* 為了保持和windows位圖兼容,步長(image_stride)必須是width_*4
		* hdc: 繪制dc
		* x_dst: 繪制面左上角x坐標
		* y_dst: 繪制面左上角y坐標
		* dst_width: 要繪制的寬度
		* dst_height: 要繪制的高度
		* x_src: 源圖像x位置
		* y_src: 原圖像y位置
		* argb_data: argb圖像數據, 格式參見前面的注釋說明
		* image_stride: 圖像每行步長
		* image_width: 圖像實際寬度
		* image_height: 圖像實際高度
		*/
		NT_UINT32(NT_API *GDIDrawARGB)(NT_HDC hdc,
			NT_INT32 x_dst, NT_INT32 y_dst,
			NT_INT32 dst_width, NT_INT32 dst_height,
			NT_INT32 x_src, NT_INT32 y_src,
			NT_INT32 src_width, NT_INT32 src_height,
			const NT_BYTE* argb_data, NT_INT32 image_stride,
			NT_INT32 image_width, NT_INT32 image_height);


	} SmartPlayerSDKAPI;


	NT_UINT32 NT_API GetSmartPlayerSDKAPI(SmartPlayerSDKAPI* pAPI);

	
	/*
	reserve1: 請傳0
	NT_PVOID: 請傳NULL
	成功返回: NT_ERC_OK
	*/
	NT_UINT32 NT_API NT_SP_SetSDKClientKey(NT_PCSTR cid, NT_PCSTR key, NT_INT32 reserve1, NT_PVOID reserve2);


#ifdef __cplusplus
}
#endif

smart_player_define.h

#ifndef SMART_PLAYER_DEFINE_H_
#define SMART_PLAYER_DEFINE_H_

#ifdef WIN32
#include <windows.h>
#endif

#ifdef SMART_HAS_COMMON_DIC
#include "../../topcommon/nt_type_define.h"
#include "../../topcommon/nt_base_code_define.h"
#else
#include "nt_type_define.h"
#include "nt_base_code_define.h"
#endif

#ifdef __cplusplus
extern "C"{
#endif

#ifndef NT_HWND_
#define NT_HWND_
#ifdef WIN32
typedef HWND NT_HWND;
#else
typedef void* NT_HWND;
#endif
#endif


#ifndef NT_HDC_
#define NT_HDC_

#ifdef _WIN32
typedef HDC NT_HDC;
#else
typedef void* NT_HDC;
#endif

#endif


/*錯誤碼*/
typedef enum _SP_E_ERROR_CODE
{
	NT_ERC_SP_HWND_IS_NULL = (NT_ERC_SMART_PLAYER_SDK | 0x1), // 窗口句柄是空
	NT_ERC_SP_HWND_INVALID = (NT_ERC_SMART_PLAYER_SDK | 0x2), // 窗口句柄無效
	NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS = (NT_ERC_SMART_PLAYER_SDK | 0x3), // 太多的截圖請求
	NT_ERC_SP_WINDOW_REGION_INVALID = (NT_ERC_SMART_PLAYER_SDK | 0x4), // 窗口區域無效,可能窗口寬或者高小于1
	NT_ERC_SP_DIR_NOT_EXIST = (NT_ERC_SMART_PLAYER_SDK | 0x5), // 目錄不存在
	NT_ERC_SP_NEED_RETRY = (NT_ERC_SMART_PLAYER_SDK | 0x6), // 需要重試
} SP_E_ERROR_CODE;


/*設置參數ID, 這個目前這么寫,SmartPlayerSDK 已經劃分范圍*/
typedef enum _SP_E_PARAM_ID
{
	SP_PARAM_ID_BASE = NT_PARAM_ID_SMART_PLAYER_SDK,
	
} SP_E_PARAM_ID;


/*事件ID*/
typedef enum _NT_SP_E_EVENT_ID
{
	NT_SP_E_EVENT_ID_BASE = NT_EVENT_ID_SMART_PLAYER_SDK,

	NT_SP_E_EVENT_ID_CONNECTING				= NT_SP_E_EVENT_ID_BASE | 0x2,	/*連接中*/
	NT_SP_E_EVENT_ID_CONNECTION_FAILED		= NT_SP_E_EVENT_ID_BASE | 0x3,	/*連接失敗*/
	NT_SP_E_EVENT_ID_CONNECTED				= NT_SP_E_EVENT_ID_BASE | 0x4,	/*已連接*/
	NT_SP_E_EVENT_ID_DISCONNECTED			= NT_SP_E_EVENT_ID_BASE | 0x5,	/*斷開連接*/
	NT_SP_E_EVENT_ID_NO_MEDIADATA_RECEIVED	= NT_SP_E_EVENT_ID_BASE | 0x8,	/*收不到RTMP數據*/
	NT_SP_E_EVENT_ID_RTSP_STATUS_CODE       = NT_SP_E_EVENT_ID_BASE | 0xB,  /*rtsp status code上報, 目前只上報401, param1表示status code*/
	NT_SP_E_EVENT_ID_NEED_KEY               = NT_SP_E_EVENT_ID_BASE | 0xC,  /*需要輸入解密key才能播放*/
	NT_SP_E_EVENT_ID_KEY_ERROR              = NT_SP_E_EVENT_ID_BASE | 0xD,  /*解密key不正確*/


	/* 接下來請從0x81開始*/
	NT_SP_E_EVENT_ID_START_BUFFERING = NT_SP_E_EVENT_ID_BASE | 0x81, /*開始緩沖*/
	NT_SP_E_EVENT_ID_BUFFERING		 = NT_SP_E_EVENT_ID_BASE | 0x82, /*緩沖中, param1 表示百分比進度*/
	NT_SP_E_EVENT_ID_STOP_BUFFERING  = NT_SP_E_EVENT_ID_BASE | 0x83, /*停止緩沖*/

	NT_SP_E_EVENT_ID_DOWNLOAD_SPEED  = NT_SP_E_EVENT_ID_BASE | 0x91, /*下載速度, param1表示下載速度,單位是(Byte/s)*/

	NT_SP_E_EVENT_ID_PLAYBACK_REACH_EOS		= NT_SP_E_EVENT_ID_BASE | 0xa1, /*播放結束, 直播流沒有這個事件,點播流才有*/
	NT_SP_E_EVENT_ID_RECORDER_REACH_EOS		= NT_SP_E_EVENT_ID_BASE | 0xa2, /*錄像結束, 直播流沒有這個事件, 點播流才有*/
	NT_SP_E_EVENT_ID_PULLSTREAM_REACH_EOS   = NT_SP_E_EVENT_ID_BASE | 0xa3, /*拉流結束, 直播流沒有這個事件,點播流才有*/

	NT_SP_E_EVENT_ID_DURATION				= NT_SP_E_EVENT_ID_BASE | 0xa8, /*視頻時長,如果是直播,則不上報,如果是點播的話, 若能從視頻源獲取視頻時長的話,則上報, param1表示視頻時長,單位是毫秒(ms)*/

} NT_SP_E_EVENT_ID;


//定義視頻幀圖像格式
typedef enum _NT_SP_E_VIDEO_FRAME_FORMAT
{
	NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一個字節保留, 內存字節格式為: bb gg rr xx, 主要是和windows位圖匹配, 在小端模式下,按DWORD類型操作,最高位是xx, 依次是rr, gg, bb
	NT_SP_E_VIDEO_FRAME_FORMAT_ARGB  = 2, // 32位的argb格式,內存字節格式是: bb gg rr aa 這種類型,和windows位圖匹配
	NT_SP_E_VIDEO_FRAME_FROMAT_I420  = 3, // YUV420格式, 三個分量保存在三個面上
} NT_SP_E_VIDEO_FRAME_FORMAT;


// 定義視頻幀結構.
typedef struct _NT_SP_VideoFrame
{
	NT_INT32  format_;  // 圖像格式, 請參考NT_SP_E_VIDEO_FRAME_FORMAT
	NT_INT32  width_;   // 圖像寬
	NT_INT32  height_;  // 圖像高

	NT_UINT64 timestamp_; // 時間戳, 一般是0,不使用, 以ms為單位的

	// 具體的圖像數據, argb和rgb32只用第一個, I420用前三個
	NT_UINT8* plane0_;
	NT_UINT8* plane1_;
	NT_UINT8* plane2_;
	NT_UINT8* plane3_;

	// 每一個平面的每一行的字節數,對于argb和rgb32,為了保持和windows位圖兼容,必須是width_*4
	// 對于I420, stride0_ 是y的步長, stride1_ 是u的步長, stride2_ 是v的步長,
	NT_INT32  stride0_;
	NT_INT32  stride1_;
	NT_INT32  stride2_;
	NT_INT32  stride3_;

} NT_SP_VideoFrame;


// 如果三項都是0的話,將不能啟動錄像
typedef struct _NT_SP_RecorderFileNameRuler
{
	NT_UINT32	type_; // 這個值目前默認是0,將來擴展用
	NT_PCSTR	file_name_prefix_; // 設置一個錄像文件名前綴, 例如:daniulive
	NT_INT32	append_date_; // 如果是1的話,將在文件名上加日期, 例如:daniulive-2017-01-17
	NT_INT32	append_time_; //  如果是1的話,將增加時間,例如:daniulive-2017-01-17-17-10-36
} NT_SP_RecorderFileNameRuler;


/*
*拉流吐視頻數據時,一些相關的數據
*/
typedef struct _NT_SP_PullStreamVideoDataInfo
{
	NT_INT32  is_key_frame_; /* 1:表示關鍵幀, 0:表示非關鍵幀 */
	NT_UINT64 timestamp_;	/* 解碼時間戳, 單位是毫秒 */
	NT_INT32  width_;	/* 一般是0 */
	NT_INT32  height_; /* 一般也是0 */
	NT_BYTE*  parameter_info_; /* 一般是NULL */
	NT_UINT32 parameter_info_size_; /* 一般是0 */
	NT_UINT64 presentation_timestamp_; /*顯示時間戳, 這個值要大于或等于timestamp_, 單位是毫秒*/

} NT_SP_PullStreamVideoDataInfo;


/*
*拉流吐音頻數據時,一些相關的數據
*/
typedef struct _NT_SP_PullStreamAuidoDataInfo
{
	NT_INT32  is_key_frame_; /* 1:表示關鍵幀, 0:表示非關鍵幀 */
	NT_UINT64 timestamp_;	/* 單位是毫秒 */
	NT_INT32  sample_rate_;	/* 一般是0 */
	NT_INT32  channel_; /* 一般是0 */
	NT_BYTE*  parameter_info_; /* 如果是AAC的話,這個是有值的, 其他編碼一般忽略 */
	NT_UINT32 parameter_info_size_; /*如果是AAC的話,這個是有值的, 其他編碼一般忽略 */
	NT_UINT64 reserve_; /* 保留  */

} NT_SP_PullStreamAuidoDataInfo;


/*
當播放器得到時候視頻大小后,會回調
*/
typedef NT_VOID(NT_CALLBACK *SP_SDKVideoSizeCallBack)(NT_HANDLE handle, NT_PVOID user_data,
	NT_INT32 width, NT_INT32 height);

/*
調用Start時傳入, 回調接口
*/
typedef NT_VOID(NT_CALLBACK *SP_SDKStartPlayCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 result);


/*
視頻圖像回調
status:目前不用,默認是0,將來可能會用
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKVideoFrameCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 status,
	const NT_SP_VideoFrame* frame);


/*
音頻PCM數據回調, 目前每幀長度是10ms
status:目前不用,默認是0,將來可能會用
data: PCM 數據
size: 數據大小
sample_rate: 采樣率
channel: 通道數
per_channel_sample_number: 每個通道的采樣數
*/
typedef NT_VOID(NT_CALLBACK* NT_SP_SDKAudioPCMFrameCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 status,
	NT_BYTE* data, NT_UINT32 size,
	NT_INT32 sample_rate, NT_INT32 channel, NT_INT32 per_channel_sample_number);

/*
截屏回調
result: 如果截屏成功的話,result是NT_ERC_OK,其他錯誤
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKCaptureImageCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 result,
	NT_PCSTR file_name);


/*
繪制視頻時,視頻幀時間戳回調, 這個用在一些特殊場景下,沒有特殊需求的用戶不需要關注
timestamp: 單位是毫秒
reserve1: 保留參數
reserve2: 保留參數
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKRenderVideoFrameTimestampCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT64 timestamp,
	NT_UINT64 reserve1, NT_PVOID reserve2);


/*
錄像回調
status: 1:表示開始寫一個新錄像文件. 2:表示已經寫好一個錄像文件
file_name: 實際錄像文件名
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKRecorderCallBack)(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 status,
	NT_PCSTR file_name);


/*
*拉流時,視頻數據回調
video_codec_id: 請參考NT_MEDIA_CODEC_ID
data: 視頻數據
size: 視頻數據大小
info: 視頻數據相關信息
reserve: 保留參數
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKPullStreamVideoDataCallBack)(NT_HANDLE handle, NT_PVOID user_data,
	NT_UINT32 video_codec_id, NT_BYTE* data, NT_UINT32 size, 
	NT_SP_PullStreamVideoDataInfo* info,
	NT_PVOID reserve);


/*
*拉流時,音頻數據回調
auido_codec_id: 請參考NT_MEDIA_CODEC_ID
data: 音頻數據
size: 音頻數據大小
info: 音頻數據相關信息
reserve: 保留參數
*/
typedef NT_VOID(NT_CALLBACK* SP_SDKPullStreamAudioDataCallBack)(NT_HANDLE handle, NT_PVOID user_data,
	NT_UINT32 auido_codec_id, NT_BYTE* data, NT_UINT32 size, 
	NT_SP_PullStreamAuidoDataInfo* info,
	NT_PVOID reserve);


/*
*播放器事件回調
event_id: 事件ID,請參考NT_SP_E_EVENT_ID
param1 到 param6, 值的意義和具體事件ID相關, 注意如果具體事件ID沒有說明param1-param6的含義,那說明這個事件不帶參數
*/
typedef NT_VOID(NT_CALLBACK* NT_SP_SDKEventCallBack)(NT_HANDLE handle, NT_PVOID user_data,
	NT_UINT32 event_id,
	NT_INT64  param1,
	NT_INT64  param2,
	NT_UINT64 param3,
	NT_PCSTR  param4,
	NT_PCSTR  param5,
	NT_PVOID  param6
	);


/*
*
* 用戶數據回調,目前是推送端發送過來的
* data_type: 數據類型,1:表示二進制字節類型. 2:表示utf8字符串 
* data:實際數據, 如果data_type是1的話,data類型是const NT_BYTE*, 如果data_type是2的話,data類型是 const NT_CHAR*
* size: 數據大小
* timestamp: 視頻時間戳
* reserve1: 保留
* reserve2: 保留
* reserve3: 保留
*/
typedef NT_VOID(NT_CALLBACK* NT_SP_SDKUserDataCallBack)(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
	);


/*
*
* 視頻的sei數據回調
* data: sei 數據
* size: sei 數據大小
* timestamp:視頻時間戳
* reserve1: 保留
* reserve2: 保留
* reserve3: 保留
* 注意: 目前測試發現有些視頻有好幾個sei nal, 為了方便用戶處理,我們把解析到的所有sei都吐出來,sei nal之間還是用 00 00 00 01 分隔, 這樣方便解析
* 吐出來的sei數據目前加了 00 00 00 01 前綴
*/
typedef NT_VOID(NT_CALLBACK* NT_SP_SDKSEIDataCallBack)(NT_HANDLE handle, NT_PVOID user_data,
	NT_BYTE*  data,
	NT_UINT32 size,
	NT_UINT64 timestamp,
	NT_UINT64 reserve1,
	NT_INT64  reserve2,
	NT_PVOID  reserve3
	);



#ifdef __cplusplus
}
#endif

#endif

總結

總的來說,無論是基于開源播放器二次開發,還是全自研,一個好的RTMP播放器或RTSP播放器,設計的時候,更多考慮的應該是如何做的更靈活、穩定,單純的幾個接口,很難滿足通用化的產品訴求。

以下共勉:厚積薄發,登上山頂,不是為了飽覽風光,是為了尋找更高的山峰!

污污直播app-污污直播破解版永久免费版