ダイアログ オーディオとビデオ 丹生兄弟: RTSP|RTMP ライブ ブロードキャスト プレーヤーを開発するのは難しいですか? 何が難しいのですか?

私がフォローしているプレーヤーの指標

多くの開発者がオーディオおよびビデオ関連テクノロジについて私に連絡するとき、彼らがよく私に尋ねる質問は、商用レベルの RTMP または RTSP プレーヤーの開発にはどのくらい時間がかかりますか?というものです。ミリ秒の遅延を実現するにはどうすればよいでしょうか? なぜプレーヤーはこれほど複雑なのでしょうか? これらの質問を念頭に置き、Windows プラットフォームの RTMP および RTSP 再生モジュールと組み合わせて、私の経験のいくつかを説明し、不適切な点があれば指摘します。

1. 低レイテンシ:ほとんどの RTMP または RTSP 再生はライブ ブロードキャスト シナリオ用です。遅延が大きすぎると、エクスペリエンスに深刻な影響を及ぼします。そのため、低レイテンシは、優れたライブ ブロードキャスト プレーヤーを評価するための非常に重要な指標です。 RTMP ライブ ブロードキャストに興味があります。印象は依然として 3 ~ 5 秒の遅延に固定されています。実際、2015 年に開発された RTMP 再生から判断すると、遅延はわずか数百ミリ秒です。一部の強いインタラクティブなシーンでは、低めの設定を使用します。 -レイテンシーモードは約200ミリ秒、RTSPも可能で、遅延もミリ秒レベルであり、インテリジェントロボットやドローンなど、制御が必要な一部のシーンでは、実際の使用後のシーンの需要を満たすために使用できます。

2. 音声と映像の同期処理:低遅延を追求するため、ほとんどのプレーヤーは音声と映像の同期すら行わず、音声と映像を取得して直接再生するため、音声と映像の同期がずれたり、ランダムに再生されるなどの様々な問題が発生します。タイムスタンプのジャンプ. したがって、優れたライブ ブロードキャスト プレーヤーには、タイムスタンプの同期と異常なタイムスタンプ修正メカニズムが必要です. もちろん、超低遅延モードの場合は、バッファーが 0 でオーディオとビデオの同期がなくてもかまいません:

3. マルチインスタンスのサポート:マルチインスタンス RTMP および RTSP 再生は、4-8-16 高解像度再生などのライブ ブロードキャスト プレーヤーを測定するための重要な指標です。

4. バッファ時間設定のサポート:ネットワーク ジッターのある一部のシナリオでは、プレーヤーはバッファ時間設定をサポートする必要があります。

5. リアルタイムの音量調整:たとえば、複数のウィンドウで RTMP または RTSP ストリームを再生する場合、すべてのオーディオが再生されるとエクスペリエンスが非常に悪くなるため、リアルタイムのミュート機能が非常に必要です。

6. ビデオ ビューの回転:一部のハードウェア デバイスは、インストール上の制限により画像が反転または回転するため、優れた RTMP および RTSP プレーヤーはビデオ ビューのリアルタイム回転 (0° 90° 180° 270) をサポートする必要があります。 °) と水平反転、垂直反転。

7. デコードされたオーディオ/ビデオ データ出力のサポート:多くの開発者は、再生中に YUV または RGB データを取得し、顔照合などのアルゴリズム分析を実行することを望んでいます。非常に重要な要求の 1 つは、データを効率的に取得することです。

8. リアルタイム スナップショット:興味深いシーンや重要なシーンをリアルタイムでキャプチャすることが非常に必要です。

9. ネットワーク ジッター処理 (ネットワークの切断と再接続など):安定したネットワーク処理メカニズムと、ネットワークの切断と再接続などのサポートは、ライブ ブロードキャスト プレーヤーにとって非常に重要です。

10. 長期的な動作安定性: 7*24 時間の使用シナリオは非常に一般的であり、長期的な動作安定性の重要性は自明です。

11. リアルタイムのダウンロード速度フィードバック:オーディオおよびビデオ ストリームにリアルタイムのダウンロード コールバックを提供しますコールバック時間間隔を設定して、ネットワーク ステータスを監視するためのリアルタイムのダウンロード速度フィードバックを確保できます。

12. 例外ステータス処理、イベント ステータス コールバック:再生中にネットワークが中断された場合、当社が提供するプレーヤーは、上位モジュールが処理を確実に認識できるように、関連するステータスをリアルタイムでコールバックできます。オープン ソース プレーヤーはこれをサポートしていません。良い;

13. ビデオ充填モードを設定します (均等な比率で表示):多くの場合、一部のシーンではビュー全体を再生する必要があります。場合によっては、ビデオが伸びるのを防ぐために、均等な比率で表示するように設定できます。 ;

14. D3D 検出:一般的に、市販されているほとんどの Windows は D3D をサポートしており、一部のニッチな Windows は GDI モード描画のみをサポートしているため、互換性を高めるために、このインターフェイスは非常に必要です。

15.特定モデルのハード デコーディング:特定モデルのハード デコーディングは、主にマルチ チャネル再生シナリオで使用され、ハード デコーディングを通じて CPU 使用率の削減を実現します。

16. キー フレームのみを再生する:特に大画面のマルチインスタンス シーンを再生する場合、CPU 使用率は非常に低くなりますが、おおよそのモニタリング状況を表示するだけで、より多くの再生チャンネルを達成したい場合は、再生のみを行うのが非常に良いです。キー フレーム機能ポイント: オリジナルのフレームを再生する必要がある場合は、リアルタイムで調整できます。

17. TCP-UDP 設定:一部のサーバー、ハードウェア デバイス、またはネットワーク環境が TCP または UDP をより適切にサポートしていることを考慮して、設定インターフェイスを追加しました。

18. TCP-UDP 自動切り替え:これはより詳細なインターフェイスです。たとえば、デフォルトで TCP モードが設定されています。TCP モードではデータを受信できません。タイムアウト後、自動的に UDP モードに切り替えて試行します。一般的に、オープンソース プレーヤーにはこの機能がありません。

19. RTSP タイムアウト設定:たとえば、10 ~ 12 秒以内にデータを受信できない場合、自動的に再接続しますが、一般にオープン ソース プレーヤーはこれを十分にサポートしていません。

技術的な実現

この記事では、Daniu Live SDK の Windows プラットフォーム C++ デモを例として、RTMP、RTSP 再生、録音、リアルタイムの音量調整、スナップショットなどのインターフェイスの設計と処理について説明します。

モジュールの初期化:

	GetSmartPlayerSDKAPI(&player_api_);

	if ( NT_ERC_OK != player_api_.Init(0, NULL) )
	{
		return FALSE;
	}

	is_support_h264_hardware_decoder_ = NT_ERC_OK == player_api_.IsSupportH264HardwareDecoder();
	is_support_h265_hardware_decoder_ = NT_ERC_OK == player_api_.IsSupportH265HardwareDecoder();

	if ( NT_ERC_OK != player_api_.Open(&player_handle_, NULL, 0, NULL) )
	{
		return FALSE;
	}

	player_api_.SetEventCallBack(player_handle_, GetSafeHwnd(), &NT_SP_SDKEventHandle);

その他のパラメータの初期化:

bool CSmartPlayerDlg::InitCommonSDKParam()
{
	ASSERT(!is_playing_);
	ASSERT(!is_recording_);

	if ( NULL == player_handle_ )
		return false;

	CString wbuffer_str;
	edit_buffer.GetWindowTextW(wbuffer_str);

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

	auto buffer_str = conv.to_bytes(wbuffer_str);

	player_api_.SetBuffer(player_handle_, atoi(buffer_str.c_str()));

	// 设置rtsp 超时时间
	player_api_.SetRtspTimeout(player_handle_, rtsp_conf_info_.timeout_);

	// 设置rtsp tcp模式,rtmp不使用, 可以不设置
	player_api_.SetRTSPTcpMode(player_handle_, rtsp_conf_info_.is_tcp_ ? 1 : 0);

	player_api_.SetRtspAutoSwitchTcpUdp(player_handle_, rtsp_conf_info_.is_tcp_udp_auto_switch_ ? 1 : 0);

	if ( btn_check_fast_startup_.GetCheck() == BST_CHECKED )
	{
		player_api_.SetFastStartup(player_handle_, 1);
	}
	else
	{
		player_api_.SetFastStartup(player_handle_, 0);
	}

	player_api_.SetReportDownloadSpeed(player_handle_, 1, 1);

	if (NT_ERC_OK != player_api_.SetURL(player_handle_, GetURL().c_str()))
	{
		return false;
	}

	connection_status_	= 0;
	buffer_status_		= 0;
	buffer_percent_		= 0;
	download_speed_		= -1;

	return true;
}

再生コントロール:

void CSmartPlayerDlg::OnBnClickedButtonPlay()
{
	if ( player_handle_ == NULL )
		return;

	CString btn_play_str;

	btn_play_.GetWindowTextW(btn_play_str);

	if ( btn_play_str == _T("播放") )
	{
		if ( !is_recording_ )
		{
			if ( !InitCommonSDKParam() )
			{
				AfxMessageBox(_T("设置参数错误!"));
				return;
			}
		}
	
		player_api_.SetVideoSizeCallBack(player_handle_, GetSafeHwnd(), SP_SDKVideoSizeHandle);

		bool is_support_d3d_render = false;
		NT_INT32 in_support_d3d_render = 0;

		if ( NT_ERC_OK == player_api_.IsSupportD3DRender(player_handle_,
			wrapper_render_wnd_.RenderWnd(), &in_support_d3d_render))
		{
			if ( 1 == in_support_d3d_render )
			{
				is_support_d3d_render = true;
			}
		}

		if ( is_support_d3d_render )
		{
			is_gdi_render_ = false;

			// 支持d3d绘制的话,就用D3D绘制
			player_api_.SetRenderWindow(player_handle_, wrapper_render_wnd_.RenderWnd());

			player_api_.SetRenderScaleMode(player_handle_, btn_check_render_scale_mode_.GetCheck() == BST_CHECKED ? 1 : 0);
		}
		else
		{
			is_gdi_render_ = true;

			// 不支持D3D就让播放器吐出数据来,用GDI绘制

			wrapper_render_wnd_.SetRenderScaleMode(btn_check_render_scale_mode_.GetCheck() == BST_CHECKED ? 1 : 0);

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

		if ( BST_CHECKED == btn_check_hardware_decoder_.GetCheck() )
		{
			player_api_.SetH264HardwareDecoder(player_handle_, is_support_h264_hardware_decoder_?1:0, 0);
			player_api_.SetH265HardwareDecoder(player_handle_, is_support_h265_hardware_decoder_?1:0, 0);
		}
		else
		{
			player_api_.SetH264HardwareDecoder(player_handle_, 0, 0);
			player_api_.SetH265HardwareDecoder(player_handle_, 0, 0);
		}

		player_api_.SetOnlyDecodeVideoKeyFrame(player_handle_, BST_CHECKED == btn_check_only_decode_video_key_frame_.GetCheck() ? 1 : 0);

		player_api_.SetLowLatencyMode(player_handle_, BST_CHECKED == btn_check_low_latency_.GetCheck() ? 1 : 0);

		player_api_.SetFlipVertical(player_handle_, BST_CHECKED == btn_check_flip_vertical_.GetCheck() ? 1 :0 );

		player_api_.SetFlipHorizontal(player_handle_, BST_CHECKED == btn_check_flip_horizontal_.GetCheck() ? 1 : 0);

		player_api_.SetRotation(player_handle_, rotate_degrees_);

		player_api_.SetAudioVolume(player_handle_, slider_audio_volume_.GetPos());

		player_api_.SetUserDataCallBack(player_handle_, GetSafeHwnd(), NT_SP_SDKUserDataHandle);

		if (NT_ERC_OK != player_api_.StartPlay(player_handle_))
		{
			AfxMessageBox(_T("播放器失败!"));
			return;
		}

		btn_play_.SetWindowTextW(_T("停止"));
		is_playing_ = true;
	}
	else
	{
		StopPlayback();
	}
}

ライブスナップショット:

void CSmartPlayerDlg::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)
	{
		// 发送截图请求成功
	}
	else if (NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS == ret)
	{
		// 通知用户延时
		OutputDebugStringA("Too many capture image requests!!!\r\n");
	}
	else
	{
		// 其他失败
	}
}

キーフレームのみを解決します。

void CSmartPlayerDlg::OnBnClickedCheckOnlyDecodeVideoKeyFrame()
{
	if (player_handle_ != NULL)
	{
		player_api_.SetOnlyDecodeVideoKeyFrame(player_handle_, BST_CHECKED == btn_check_only_decode_video_key_frame_.GetCheck() ? 1 : 0);
	}
}

比例表示またはフルタイルモードを設定します。

void CSmartPlayerDlg::OnBnClickedCheckRenderScaleMode()
{
	if (player_handle_ != NULL)
	{
		if (!is_gdi_render_)
		{
			player_api_.SetRenderScaleMode(player_handle_, BST_CHECKED == btn_check_render_scale_mode_.GetCheck() ? 1 : 0);
		}
		else
		{
			wrapper_render_wnd_.SetRenderScaleMode(btn_check_render_scale_mode_.GetCheck() == BST_CHECKED ? 1 : 0);
		}
	}
}

ビデオビューの水平反転、垂直反転、回転:

void CSmartPlayerDlg::OnBnClickedCheckFlipHorizontal()
{
	if (player_handle_ != NULL)
	{
		player_api_.SetFlipHorizontal(player_handle_, BST_CHECKED == btn_check_flip_horizontal_.GetCheck() ? 1 : 0);
	}
}

void CSmartPlayerDlg::OnBnClickedCheckFlipVertical()
{
	if (player_handle_ != NULL)
	{
		player_api_.SetFlipVertical(player_handle_, BST_CHECKED == btn_check_flip_vertical_.GetCheck() ? 1 : 0);
	}
}

void CSmartPlayerDlg::OnBnClickedButtonRotation()
{
	rotate_degrees_ += 90;
	rotate_degrees_ = rotate_degrees_ % 360;

	if (0 == rotate_degrees_)
	{
		btn_rotation_.SetWindowText(_T("旋转90度"));
	}
	else if (90 == rotate_degrees_)
	{
		btn_rotation_.SetWindowText(_T("旋转180度"));
	}
	else if (180 == rotate_degrees_)
	{
		btn_rotation_.SetWindowText(_T("旋转270度"));
	}
	else if (270 == rotate_degrees_)
	{
		btn_rotation_.SetWindowText(_T("不旋转"));
	}

	if ( player_handle_ != NULL )
	{
		player_api_.SetRotation(player_handle_, rotate_degrees_);
	}
}

リアルタイム録音:

void CSmartPlayerDlg::OnBnClickedButtonRecord()
{
	if ( player_handle_ == NULL )
		return;

	CString btn_record_str;
	btn_record_.GetWindowTextW(btn_record_str);

	if ( btn_record_str == _T("录像") )
	{
		if ( !rec_conf_info_.is_record_video_ && !rec_conf_info_.is_record_audio_ )
		{
			AfxMessageBox(_T("音频录制选项和视频录制选项至少需要选择一个!"));
			return;
		}

		if ( !is_playing_ )
		{
			if ( !InitCommonSDKParam() )
			{
				AfxMessageBox(_T("设置参数错误!"));
				return;
			}
		}

		player_api_.SetRecorderVideo(player_handle_, rec_conf_info_.is_record_video_ ? 1 : 0);
		player_api_.SetRecorderAudio(player_handle_, rec_conf_info_.is_record_audio_ ? 1 : 0);

		auto ret = player_api_.SetRecorderDirectory(player_handle_, rec_conf_info_.dir_.c_str());
		if ( NT_ERC_OK != ret )
		{
			AfxMessageBox(_T("设置录像目录失败,请确保目录存在且是英文目录"));
			return;
		}

		player_api_.SetRecorderFileMaxSize(player_handle_, rec_conf_info_.file_max_size_);

		NT_SP_RecorderFileNameRuler rec_name_ruler = { 0 };

		rec_name_ruler.type_ = 0;
		rec_name_ruler.file_name_prefix_ = rec_conf_info_.file_name_prefix_.c_str();
		rec_name_ruler.append_date_		 = rec_conf_info_.is_append_date_ ? 1 : 0;
		rec_name_ruler.append_time_		 = rec_conf_info_.is_append_time_ ? 1 : 0;

		player_api_.SetRecorderFileNameRuler(player_handle_, &rec_name_ruler);

		player_api_.SetRecorderCallBack(player_handle_, GetSafeHwnd(), &SP_SDKRecorderHandle);

		player_api_.SetRecorderAudioTranscodeAAC(player_handle_, rec_conf_info_.is_audio_transcode_aac_ ? 1 : 0);

		if ( NT_ERC_OK != player_api_.StartRecorder(player_handle_) )
		{
			AfxMessageBox(_T("录像失败!"));
			return;
		}

		btn_record_.SetWindowTextW(_T("停止录像"));
		is_recording_ = true;
	}
	else
	{
		StopRecorder();
	}
}

イベントコールバック:

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;
	}
	else if ( NT_SP_E_EVENT_ID_RECORDER_REACH_EOS == event_id )
	{
		StopRecorder();
		return S_OK;
	}
	else if ( NT_SP_E_EVENT_ID_RTSP_STATUS_CODE == event_id )
	{
		int status_code = (int)lParam;
		if ( 401 == status_code )
		{
			HandleVerification();
		}

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

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

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

		return S_OK;
	}
	else if ( 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");
		}
		else if ( NT_SP_E_EVENT_ID_CONNECTION_FAILED == event_id )
		{
			OutputDebugStringA("connection status: connection failed\r\n");
		}
		else if ( NT_SP_E_EVENT_ID_CONNECTED == event_id )
		{
			OutputDebugStringA("connection status: connected\r\n");
		}
		else if (NT_SP_E_EVENT_ID_DISCONNECTED == event_id)
		{
			OutputDebugStringA("connection status: disconnected\r\n");
		}
		else if (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;
	}

	CString show_str = base_title_;

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

		if ( NT_SP_E_EVENT_ID_CONNECTING == connection_status_ )
		{
			show_str += _T("链接中");
		}
		else if ( NT_SP_E_EVENT_ID_CONNECTION_FAILED == connection_status_ )
		{
			show_str += _T("链接失败");
		}
		else if ( NT_SP_E_EVENT_ID_CONNECTED == connection_status_ )
		{
			show_str += _T("链接成功");
		}
		else if ( NT_SP_E_EVENT_ID_DISCONNECTED == connection_status_ )
		{
			show_str += _T("链接断开");
		}
		else if (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("开始缓冲");
		}
		else if (NT_SP_E_EVENT_ID_BUFFERING == buffer_status_)
		{
			std::wostringstream ss;
			ss << L"缓冲中" << buffer_percent_ << "%";
			show_str += ss.str().c_str();
		}
		else if (NT_SP_E_EVENT_ID_STOP_BUFFERING == buffer_status_)
		{
			show_str += _T("结束缓冲");
		}
	}


	SetWindowText(show_str);

	return S_OK;
}

要約する

RTMP や RTSP プレーヤーを作るのは簡単だとよく言われますが、安定性が高く、遅延が少なく、強力なライブ ブロードキャスト プレーヤーを作るのは、特に細部の処理において依然として困難であり、オープンソース プレーヤーには大きな問題はありません。このシナリオにはまだ多くの欠点があり、プレーヤーを作成するのは簡単ですが、優れたプレーヤーを作成するのは困難です。上記はほんのちょっとした経験談ですので、興味のある開発者は参考にしてみてください。

おすすめ

転載: blog.csdn.net/renhui1112/article/details/132391347
おすすめ