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

开发背景

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

  • **事件回调**\]支持网络状态、buffer状态等回调;

  • **音频格式**\]支持AAC/PCMA/PCMU/Speex;

  • **H.264硬解码** \]**Windows**/Android/iOS支持H.264硬解;

  • **H.264/H.265硬解码**\]Android支持设置Sur face模式硬解和普通模式硬解码;

  • **首屏秒开**\]支持首屏秒开模式(RTMP服务器缓存GOP的情况);

  • **复杂网络处理**\]支持断网重连等各种网络环境自动适配;

  • **音视频多种render机制**\]Android平台,视频:sur faceview/OpenGL ES,音频:AudioTrack/OpenSL ES;

  • **实时音量调节**\]支持播放过程中,实时调节播放音量,调节范围\[0, 100\];

  • **只播关键帧**\]Windows平台支持实时设置是否只播放关键帧;

  • **渲染镜像**\]支持水平反转、垂直反转模式设置;

  • **ARGB叠加**\]Windows平台支持ARGB图像叠加到显示视频(参看C++的DEMO);

  • **解码后视频数据回调**\]支持解码后YUV/RGB数据回调;

  • **解码前音频数据回调**\]支持AAC/PCMA/PCMU/SPEEX数据回调;

  • **扩展录像功能**\]支持RTMP H.264、扩展H.265流录制,支持PCMA/PCMU/Speex转AAC后录制,支持设置只录制音频或视频等;

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

1. Init/UnInit()接口

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

scss 复制代码
/*
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()接口时,需要确保录像、拉流都正常停止后,再调用。

scss 复制代码
/*
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. 网络状态回调

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

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

demo实现实例:

ini 复制代码
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平台如果同时播放的实例不多或者分辨率不是太高的话,考虑到播放体验,建议优先考虑软解码,如果特定设备需要多路播放,也可以考虑硬解,需要注意的是,如果调用硬解码,需要先做是否支持硬解码检测,接口如下:

scss 复制代码
/*
检查是否支持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端,好多场景下,因为需要播放非常多路,但是又不想占用太多的系统资源,如果全帧播放,路数过多,全部解码、绘制,系统资源占用会加大,如果能灵活的处理,可以随时只播放关键帧,全帧播放切换,对系统性能要求大幅降低,想全帧播放的时候,随时切换全帧绘制。

scss 复制代码
/*
*设置只解码视频关键帧
*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,会增大播放延迟,好处是,网络抖动的时候,流畅性更好。

scss 复制代码
/*
设置buffer,最小0ms
*/NT_UINT32(NT_API *SetBuffer)(NT_HANDLE handle, NT_INT32 buffer);

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

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

scss 复制代码
/*
静音接口,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。

相关接口设计如下:

css 复制代码
player_api_.SetRenderScaleMode(player_handle_, btn_check_render_scale_mode_.GetCheck() == BST_CHECKED ? 1 : 0);

9.快速启动

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

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

10. 低延迟模式

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

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

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

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

scss 复制代码
/*
*上下反转(垂直反转)
*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更友好的交互。

scss 复制代码
/*
设置下载速度上报, 默认不上报下载速度
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. 实时快照

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

scss 复制代码
/*
捕获图片
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);

调用实例如下:

c 复制代码
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后再录像。

scss 复制代码
/*
* 设置是否录视频,默认的话,如果视频源有视频就录,没有就没得录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
* 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服务。

scss 复制代码
/*
* 设置拉流时,吐视频数据的回调
*/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回调接口即可。

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

调用实例如下:

arduino 复制代码
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模式绘制:

ini 复制代码
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%就足够满足特定场景使用了。

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

相关推荐
aqi006 天前
FFmpeg开发笔记(九十九)基于Kotlin的国产开源播放器DKVideoPlayer
android·ffmpeg·kotlin·音视频·直播·流媒体
字节架构前端8 天前
媒体采集标准草案 与 Chromium 音频采集实现简介
前端·chrome·音视频开发
Tiny_React12 天前
使用 Claude Code Skills 模拟的视频生成流程
人工智能·音视频开发·vibecoding
aqi0012 天前
FFmpeg开发笔记(九十八)基于FFmpeg的跨平台图形用户界面LosslessCut
android·ffmpeg·kotlin·音视频·直播·流媒体
aqi0013 天前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
aqi0014 天前
FFmpeg开发笔记(一百)国产的Android开源视频压缩工具VideoSlimmer
android·ffmpeg·音视频·直播·流媒体
haibindev16 天前
【终极踩坑指南】Windows 10上MsQuic证书加载失败?坑不在证书,而在Schannel!
直播·http3·quic·流媒体
飞鸟真人20 天前
livekit搭建与使用浏览器测试
直播·视频会议·视频聊天·livekit
hk112420 天前
【音视频/边缘计算】2025年度H.265/HEVC高并发解码与画质修复(Super-Resolution)基准测试报告(含沙丘/失控玩家核心样本)
ffmpeg·边缘计算·音视频开发·h.265·测试数据集
aqi001 个月前
FFmpeg开发笔记(九十五)国产的开源视频美颜工具VideoEditorForAndroid
android·ffmpeg·音视频·直播·流媒体