Windows平台RTSP|RTMP播放器如何实现细粒度录像控制

​技术背景

好多开发者在跟我做技术交流的时候,说用大牛直播SDK模块的特点是,想到什么功能,找找头文件和demo几乎都有对应的实现,你们是何收集到这么多技术需求的?

实际上,这还是取决于我们多年的行业口碑和大规模的实实在在的用户积累,才让我们清楚的认识到,一个直播模块,需要有什么,需要舍弃什么。

技术设计

本文以大牛直播SDK的Windows平台RTSP|RTMP直播播放录制功能设计为例,谈谈我们的接口的细粒度设计。

目前,我们录像模块,涵盖了Windows/Linux/android/iOS 推送端(涵盖轻量级RTSP服务模块、RTMP推流模块和GB28181设备接入模块)和RTSP|RTMP播放端,主要实现了如下功能:

  • [拉流]支持拉取RTSP流录像;
  • [拉流]支持拉取RTMP流录像;
  • [推流端录像]支持RTMP|RTSP推送端同步录像;
  • [轻量级RTSP服务录像]支持轻量级RTSP服务SDK同步录像;
  • [推流端录像实时暂停/恢复]支持推送端录像过程中实时暂停录像、恢复录像;
  • [逻辑分离]大牛直播录像SDK不同于普通录像接口,更智能,和推送、播放、转发、内置轻量级RTSP服务SDK功能完全分离,支持随时录像;
  • [url切换]在录像过程中,支持切换不同URL,如两个URL配置一致,则可以录制到同一个MP4文件,如不一致,可自动分割到下一个文件;
  • [参数设置]支持设置单个录像文件大小、录像路径等,并支持纯音频、纯视频、音视频录制模式;
  • [音频转码]支持音频(PCMU/PCMA,Speex等)转AAC后再录像;
  • [265支持]支持RTSP/RTMP H.265录制到MP4文件;
  • [推送端265录像]推送端SDK支持H265录像;
  • [推送端外部编码数据对接录像]支持推送端外部编码后数据(H.264/AAC)对接录像;
  • [事件回调]从开始录像,到录像结束均有event callback上来,网络堵塞、音视频同步均做了非常友好的处理。

设置只录制视频或音频:

csharp 复制代码
        /*
		 * 设置是否录视频,默认的话,如果视频源有视频就录,没有就没得录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
         * 
		 * is_record_video: 1 表示录制视频, 0 表示不录制视频, 默认是1
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderVideo(IntPtr handle, Int32 is_record_video);


		/*
		 * 设置是否录音频,默认的话,如果视频源有音频就录,没有就没得录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关
		 *
         * is_record_audio: 1 表示录制音频, 0 表示不录制音频, 默认是1
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderAudio(IntPtr handle, Int32 is_record_audio);

录像存储位置设置,设置本地录像目录,需要注意的是,我们已经支持宽字符中文路径设置,开始录像和录像结束,我们会有事件回调上来:

arduino 复制代码
        /*
		 * 设置本地录像目录, 支持中文目录, 需要设置宽字符,比如L"D:\\xxx\\gg"
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderDirectoryW(IntPtr handle, [MarshalAs(UnmanagedType.LPWStr)] String dir);

设置单个录像文件大小,如果一直是录像状态,超过这个大小后,会自动切分录制到下个文件:

arduino 复制代码
		/*
		 * 设置单个录像文件最大大小, 当超过这个值的时候,将切割成第二个文件
		 * size: 单位是KB(1024Byte), 当前范围是 [5MB-800MB], 超出将被设置到范围内
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderFileMaxSize(IntPtr handle, UInt32 size);

设置录像名称生成规则:

csharp 复制代码
		/*
		 * 设置录像文件名生成规则
		 */
        [DllImport(@"SmartPlayerSDK.dll", EntryPoint = "NT_SP_SetRecorderFileNameRuler", CallingConvention = CallingConvention.StdCall)]
        public static extern UInt32 NT_SP_SetRecorderFileNameRuler(IntPtr handle, ref NT_SP_RecorderFileNameRuler ruler);

结构体定义如下:

csharp 复制代码
    /*如果三项都是0的话,将不能启动录像*/
    [StructLayoutAttribute(LayoutKind.Sequential)]
    public struct NT_SP_RecorderFileNameRuler
    {
        public UInt32 type_;                                          // 这个值目前默认是0,将来扩展用
        [MarshalAs(UnmanagedType.LPStr)] public String file_name_prefix_;  // 设置一个录像文件名前缀, 例如:daniulive
        public Int32 append_date_;                                    // 如果是1的话,将在文件名上加日期, 例如:daniulive-2017-01-17
        public Int32 append_time_;                                    // 如果是1的话,将增加时间,例如:daniulive-2017-01-17-17-10-36
    }

开始录像和结束录像,我们有事件回调,设置回调接口如下:

csharp 复制代码
		/*
		 * 设置录像回调接口
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderCallBack(IntPtr handle,
            IntPtr call_back_data, SP_SDKRecorderCallBack call_back);

设置录像时音频转AAC编码的开关,考虑到AAC更通用,我们增加其他音频编码(比如speex, pcmu, pcma等)转AAC的功能:

csharp 复制代码
        /*
         * 设置录像时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.
         * is_transcode: 设置为1的话,如果音频编码不是aac,则转成aac, 如果是aac,则不做转换. 设置为0的话,则不做任何转换. 默认是0.
         * 注意: 转码会增加性能消耗
		 */
		[DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_SetRecorderAudioTranscodeAAC(IntPtr handle, Int32 is_transcode);

录像控制接口,开始录像和结束录像:

csharp 复制代码
		/*
		 * 启动录像
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_StartRecorder(IntPtr handle);

		/*
		 * 停止录像
		 */
        [DllImport(@"SmartPlayerSDK.dll")]
        public static extern UInt32 NT_SP_StopRecorder(IntPtr handle);

以C#的demo为例,我们看看录像调用示例代码:

设置录像规则:

ini 复制代码
        private void btn_record_config_Click(object sender, EventArgs e)
        {
            RecordConfigForm record_config_dlg = new RecordConfigForm(is_rec_video_, is_rec_audio_, rec_dir_, rec_name_file_prefix_, max_file_size_, is_append_date_, is_append_time_, is_audio_transcode_aac_);

            record_config_dlg.ShowDialog();

            String rec_dir = record_config_dlg.RecDir();

            if (!String.IsNullOrEmpty(rec_dir))
            {
                rec_dir_ = rec_dir;
            }
            else
            {
                MessageBox.Show("未设置录像保存路径,默认保存到rec文件夹下..");
            }

            is_rec_video_ = record_config_dlg.IsRecVideo();
            is_rec_audio_ = record_config_dlg.IsRecAudio();
            rec_name_file_prefix_ = record_config_dlg.RecNameFilePrefix();
            max_file_size_ = record_config_dlg.MaxFileSize();
            is_append_date_ = record_config_dlg.IsAppendDate();
            is_append_time_ = record_config_dlg.IsAppendTime();
            is_audio_transcode_aac_ = record_config_dlg.IsAudioTanscodeAAC();
        }

开始录像和结束录像:

ini 复制代码
        private void btn_record_Click(object sender, EventArgs e)
        {
            if (player_handle_ == IntPtr.Zero)
                return;

            if (btn_record.Text == "录像")
            {
                if (!is_rec_video_ && !is_rec_audio_)
                {
                    MessageBox.Show("音频录制选项和视频录制选项至少需要选择一个!");
                    return;
                }

                if (!is_playing_)
                {
                    if (!InitCommonSDKParam())
                    {
                        MessageBox.Show("设置参数错误!");
                        return;
                    }
                }

                NTSmartPlayerSDK.NT_SP_SetRecorderVideo(player_handle_, is_rec_video_ ? 1 : 0);
                NTSmartPlayerSDK.NT_SP_SetRecorderAudio(player_handle_, is_rec_audio_ ? 1 : 0);

                UInt32 ret = NTSmartPlayerSDK.NT_SP_SetRecorderDirectoryW(player_handle_, rec_dir_);
                if (NT.NTBaseCodeDefine.NT_ERC_OK != ret)
                {
                    MessageBox.Show("设置录像目录失败");
                    return;
                }

                NTSmartPlayerSDK.NT_SP_SetRecorderFileMaxSize(player_handle_, max_file_size_);

                NT_SP_RecorderFileNameRuler rec_name_ruler = new NT_SP_RecorderFileNameRuler();

                rec_name_ruler.type_ = 0;
                rec_name_ruler.file_name_prefix_ = rec_name_file_prefix_;
                rec_name_ruler.append_date_ = is_append_date_ ? 1 : 0;
                rec_name_ruler.append_time_ = is_append_time_ ? 1 : 0;

                NTSmartPlayerSDK.NT_SP_SetRecorderFileNameRuler(player_handle_, ref rec_name_ruler);

                record_call_back_ = new SP_SDKRecorderCallBack(SDKRecorderCallBack);

                NTSmartPlayerSDK.NT_SP_SetRecorderCallBack(player_handle_, IntPtr.Zero, record_call_back_);

                NTSmartPlayerSDK.NT_SP_SetRecorderAudioTranscodeAAC(player_handle_, is_audio_transcode_aac_ ? 1 : 0);

                if (NT.NTBaseCodeDefine.NT_ERC_OK != NTSmartPlayerSDK.NT_SP_StartRecorder(player_handle_))
                {
                    MessageBox.Show("录像失败!");
                    return;
                }

                btn_record.Text = "停止录像";
                is_recording_ = true;
            }
            else
            {
                StopRecorder();
            }
        }

录像回调处理,由于我们支持宽字符中文路径,回调上来的文件路径,需要做下简单的处理:

ini 复制代码
        private void RecordCallBack(UInt32 status, [MarshalAs(UnmanagedType.LPStr)] String file_name)
        {
            byte[] utf8_bytes = Encoding.Default.GetBytes(file_name);
            byte[] default_bytes = Encoding.Convert(Encoding.UTF8, Encoding.Default, utf8_bytes);
            String recorder_file_name = Encoding.Default.GetString(default_bytes);

            StringBuilder sb = new StringBuilder();
            sb.Append("录像状态:");
            
            if (status == 1)
            {
                sb.Append("new file: ");
            }
            else if(status == 2)
            {
                sb.Append("finished file: ");
            }

            sb.Append(recorder_file_name);

            MessageBox.Show(sb.ToString());
        }

总结

上述是Window平台RTSP|RTMP直播播放录像相关的接口设计探讨,感兴趣的开发者,可以单独和我交流。有人说国内的互联网环境下,做SDK真的很难生存,是的,开源的那么多,干嘛非要用你们的?但是也有人说,目前好多传统行业,对流媒体直播这块,技术要求非常高,市面上找个靠谱的,真的太难了。专注做好一件事,极致做精一件事,口碑做成一件事,比快更快,让RTSP|RTMP直播播放器更适用于延迟要求苛刻的使用场景(如平衡控制、无人机、智能机器人等),是我们一直的追求。

相关推荐
dvlinker1 天前
【音视频开发】使用支持硬件加速的D3D11绘图遇到的绘图失败与绘图崩溃问题的记录与总结
音视频开发·c/c++·视频播放·d3d11·d3d11绘图模式
aqi004 天前
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
ffmpeg·音视频·直播·流媒体
音视频牛哥6 天前
Android平台GB28181实时回传流程和技术实现
音视频开发·视频编码·直播
音视频牛哥8 天前
RTMP、RTSP直播播放器的低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥12 天前
电脑共享同屏的几种方法分享
音视频开发·视频编码·直播
aqi0014 天前
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
android·ffmpeg·音视频·直播·流媒体
aqi0015 天前
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
android·ffmpeg·音视频·直播·流媒体
加油吧x青年16 天前
Web端开启直播技术方案分享
前端·webrtc·直播
aqi001 个月前
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
android·ffmpeg·音视频·直播·流媒体