Android平台RTSP转RTMP推送之采集麦克风音频转发

news2024/11/25 3:32:17

技术背景

RTSP转RTMP推送,好多开发者第一想到的是采用ffmpeg命令行的形式,如果对ffmpeg比较熟,而且产品不要额外的定制和更高阶的要求,未尝不可,如果对产品稳定性、时延、断网重连等有更高的技术诉求,比较好的办法,还是采用我们的技术实现。

技术实现

以大牛直播SDK的多路RTSP转RTMP推送模块为例,首先拉取RTSP流,把未解码的H.264/H.265、AAC/PCMA/PCMU数据回调上来,然后通过调用推送模块的编码后数据接口,同步转发出去,整体下来,几无多少延迟。如果需要把数据投递到轻量级RTSP服务也可以。系统设计架构图如下:

1. 拉流:通过RTSP直播播放SDK的数据回调接口,拿到音视频数据;

2. 转推:通过RTMP直播推送SDK的编码后数据输入接口,把回调上来的数据,传给RTMP直播推送模块,实现RTSP数据流到RTMP服务器的转发;

3. 录像:如果需要录像,借助RTSP直播播放SDK,拉到音视频数据后,直接存储MP4文件即可;

4. 快照:如果需要实时快照,拉流后,解码调用播放端快照接口,生成快照,因为快照涉及到video数据解码,如无必要,可不必开启,不然会额外消耗性能。

5. 拉流预览:如需预览拉流数据,只要调用播放端的播放接口,即可实现拉流数据预览;

6. 数据转AAC后转发:考虑到好多监控设备出来的音频可能是PCMA/PCMU的,如需要更通用的音频格式,可以转AAC后,在通过RTMP推送;

7. 转推RTMP实时静音:只需要在传audio数据的地方,加个判断即可;

8. 拉流速度反馈:通过RTSP播放端的实时码率反馈event,拿到实时带宽占用即可;

9. 整体网络状态反馈:考虑到有些摄像头可能会临时或异常关闭,RTMP服务器亦是,可以通过推拉流的event回调状态,查看那整体网络情况,如此界定:是拉不到流,还是推不到RTMP服务器。

多路RTMP/RTSP转RTMP推送模块功能支持:

  1. 支持拉取rtmp流;
  2. 支持拉取rtsp流;
  3. Windows支持本地flv文件转发(支持制定文件位置转发,或转发过程中seek);
  4. 支持本地预览;
  5. 支持转发过程中,实时静音;
  6. 支持转发过程中,切换rtmp/rtsp url,此外,windows平台还支持切换本地flv文件;
  7. 支持录像模块扩展,可边转发边录制,每个文件录制开始结束,均有状态回馈;
  8. 支持内网RTSP网关模块扩展,拉取的流数据,可以流入到内网RTSP网关模块,对外微型RTSP媒体流服务(RTSP url),便于内网访问;
  9. 音频:AAC,并支持拉流后的音频(PCMU/PCMA,Speex等)转AAC后再转发;
  10. 视频:H.264、H.265,支持h265转发(rtsp/rtmp h265转rtmp h265推送);

上述实现,2016年我们已经非常成熟,本次要谈的,是开发者实际场景用到的一个技术需求,如何实现视频用RTSP数据源获取到的,音频采集麦克风的数据。

废话不多说,上代码:

先说开始拉流、停止拉流设计如下,如果是用rtsp的audio,那么我们就开启audio数据的回调,如果采用麦克风的,这里只要开video的即可。

/*
 * SmartRelayDemo.java
 * Created by daniusdk.com
 * weChat: xinsheng120
 */
private boolean StartPull()
{
	if ( isPulling )
		return false;

	if(!isPlaying)
	{
		if (!OpenPullHandle())
			return false;
	}

	if(audio_opt_ == 2)
	{
		libPlayer.SmartPlayerSetAudioDataCallback(player_handle_, new PlayerAudioDataCallback(stream_publisher_));
	}
	if(video_opt_ == 2)
	{
		libPlayer.SmartPlayerSetVideoDataCallback(player_handle_, new PlayerVideoDataCallback(stream_publisher_));
	}

	int is_pull_trans_code  = 1;
	libPlayer.SmartPlayerSetPullStreamAudioTranscodeAAC(player_handle_, is_pull_trans_code);

	int startRet = libPlayer.SmartPlayerStartPullStream(player_handle_);

	if (startRet != 0) {
		Log.e(TAG, "Failed to start pull stream!");

		if(!isPlaying)
		{
			releasePlayerHandle();
		}

		return false;
	}

	isPulling = true;
	return true;
}

private void StopPull()
{
	if ( !isPulling )
		return;

	isPulling = false;

	if (null == libPlayer || 0 == player_handle_)
		return;

	libPlayer.SmartPlayerStopPullStream(player_handle_);

	if ( !isPlaying)
	{
		releasePlayerHandle();
	}
}

OpenPullHandle()实现逻辑如下,常规的参数设置,和event callback设置等。

private boolean OpenPullHandle()
{
	//playbackUrl可自定义
	playbackUrl = "rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream";

	if (playbackUrl == null) {
		Log.e(TAG, "playback URL is null...");
		return false;
	}

	player_handle_ = libPlayer.SmartPlayerOpen(context_);

	if (player_handle_ == 0) {
		Log.e(TAG, "playerHandle is null..");
		return false;
	}

	libPlayer.SetSmartPlayerEventCallbackV2(player_handle_,
			new EventHandlePlayerV2());

	libPlayer.SmartPlayerSetBuffer(player_handle_, playBuffer);

	// set report download speed
	libPlayer.SmartPlayerSetReportDownloadSpeed(player_handle_, 1, 2);

	//设置RTSP超时时间
	int rtsp_timeout = 10;
	libPlayer.SmartPlayerSetRTSPTimeout(player_handle_, rtsp_timeout);

	//设置RTSP TCP/UDP模式自动切换
	int is_auto_switch_tcp_udp = 1;
	libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(player_handle_, is_auto_switch_tcp_udp);

	libPlayer.SmartPlayerSaveImageFlag(player_handle_, 1);

	// It only used when playback RTSP stream..
	//libPlayer.SmartPlayerSetRTSPTcpMode(playerHandle, 1);

	libPlayer.SmartPlayerSetUrl(player_handle_, playbackUrl);

	return true;
}

拉流后,转推RTMP的设计如下:

btnRTMPPusher.setOnClickListener(new Button.OnClickListener() {

	// @Override
	public void onClick(View v) {

		if (stream_publisher_.is_rtmp_publishing()) {
			stopPush();

			btnRTMPPusher.setText("推送RTMP");
			return;
		}

		Log.i(TAG, "onClick start push rtmp..");
		InitAndSetConfig();

		String rtmp_pusher_url = "rtmp://192.168.0.104:1935/hls/stream1";

		//String rtmp_pusher_url = relayStreamUrl;

		if (!stream_publisher_.SetURL(rtmp_pusher_url))
			Log.e(TAG, "Failed to set publish stream URL..");

		boolean start_ret = stream_publisher_.StartPublisher();
		if (!start_ret) {
			stream_publisher_.try_release();
			Log.e(TAG, "Failed to start push stream..");
			return;
		}

		startAudioRecorder();

		btnRTMPPusher.setText("停止推送");
	}
});

InitAndSetConfig()设计如下:

private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

	Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_);

	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt_, video_opt_,  video_width_, video_height_);
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return;
	}

	Log.i(TAG, "publisherHandle=" + handle);

	int fps = 25;
	int gop = fps * 3;

	initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

	stream_publisher_.set(libPublisher, handle);
}

这里可以看到,我们在转推RTMP的时候,调用了startAudioRecorder()来做麦克风的采集:

void startAudioRecorder() {

	if(audio_opt_ != 1)
		return;

	if (audio_recorder_ != null)
		return;

	audio_recorder_ = new NTAudioRecordV2(this);

	Log.i(TAG, "startAudioRecorder call audio_recorder_.start()+++...");

	audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);

	audio_recorder_.AddCallback(audio_recorder_callback_);

	if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1) ) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;

		audio_recorder_ = null;

		Log.e(TAG, "startAudioRecorder start failed.");
	}
	else {
		Log.i(TAG, "startAudioRecorder call audio_recorder_.start() OK---...");
	}
}

void stopAudioRecorder() {
	if (null == audio_recorder_)
		return;

	Log.i(TAG, "stopAudioRecorder+++");

	audio_recorder_.Stop();

	if (audio_recorder_callback_ != null) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;
	}

	audio_recorder_ = null;

	Log.i(TAG, "stopAudioRecorder---");
}

采集到的audio回调上来后,我们调RTMP推送接口,把数据投递下去即可:

private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
	private WeakReference<LibPublisherWrapper> publisher_0_;
	private WeakReference<LibPublisherWrapper> publisher_1_;

	public NTAudioRecordV2CallbackImpl(LibPublisherWrapper publisher_0, LibPublisherWrapper publisher_1) {
		if (publisher_0 != null)
			publisher_0_ = new WeakReference<>(publisher_0);

		if (publisher_1 != null)
			publisher_1_ = new WeakReference<>(publisher_1);
	}

	private final LibPublisherWrapper get_publisher_0() {
		if (publisher_0_ !=null)
			return publisher_0_.get();

		return null;
	}

	private final LibPublisherWrapper get_publisher_1() {
		if (publisher_1_ != null)
			return publisher_1_.get();

		return null;
	}

	@Override
	public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {

		 //Log.i(TAG, "onNTAudioRecordV2Frame size=" + size + " sampleRate=" + sampleRate + " channel=" + channel
		 //			 + " per_channel_sample_number=" + per_channel_sample_number);

		LibPublisherWrapper publisher_0 = get_publisher_0();
		if (publisher_0 != null)
			publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);

		LibPublisherWrapper publisher_1 = get_publisher_1();
		if (publisher_1 != null)
			publisher_1.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);

	}
}

编码后的视频投递设计如下:

class PlayerVideoDataCallback implements NTVideoDataCallback
{
	private WeakReference<LibPublisherWrapper> publisher_;
	private int video_buffer_size = 0;
	private ByteBuffer video_buffer_ = null;

	public PlayerVideoDataCallback(LibPublisherWrapper publisher) {
		if (publisher != null)
			publisher_ = new WeakReference<>(publisher);
	}

	@Override
	public ByteBuffer getVideoByteBuffer(int size)
	{
		if( size < 1 )
		{
			return null;
		}

		if ( size <= video_buffer_size &&  video_buffer_ != null )
		{
			return  video_buffer_;
		}

		video_buffer_size = size + 1024;
		video_buffer_size = (video_buffer_size+0xf) & (~0xf);

		video_buffer_ = ByteBuffer.allocateDirect(video_buffer_size);

		return video_buffer_;
	}

	public void onVideoDataCallback(int ret, int video_codec_id, int sample_size, int is_key_frame, long timestamp, int width, int height, long presentation_timestamp)
	{
		if ( video_buffer_ == null)
			return;

		LibPublisherWrapper publisher = publisher_.get();
		if (null == publisher)
			return;

		if (!publisher.is_publishing())
			return;

		video_buffer_.rewind();

		publisher.PostVideoEncodedData(video_codec_id, video_buffer_, sample_size, is_key_frame, timestamp, presentation_timestamp);

	}
}

总结

从我发的Android平台RTSP转RTMP推送的demo界面,可以看到,这个demo,不是单纯的RTSP转RTMP推送的,还可以实现RTSP流获取后,回调上来解码后的数据,然后添加动态水印或其他处理后,把video数据二次编码推送出去。或者audio数据二次处理。

此外,还可以实现拉流的数据预览播放、把数据注入到轻量级RTSP服务模块,然后二次编码的数据,本地录像、快照等。一个好的RTSP转RTMP推送的模块,一定要足够的灵活,扩展性好,才能很快的实现客户的技术诉求。以上抛砖引玉,感兴趣的开发者,可以跟我单独探讨。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2228784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SSM旅游信息系统-计算机毕业设计源码00526

目 录 摘要 1 绪论 1.1 研究背景 1.2研究意义 1.3论文结构与章节安排 2 旅游信息系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分析 2.4 系统流…

「Mac畅玩鸿蒙与硬件7」鸿蒙开发环境配置篇7 - 使用命令行工具和本地模拟器管理项目

本篇将讲解在 macOS 上配置 HarmonyOS 开发环境的流程&#xff0c;聚焦 hvigorw 命令行工具的使用。我们将以创建 HelloWorld 项目为例&#xff0c;演示使用 hvigorw 进行项目构建、清理操作&#xff0c;并通过 DevEco Studio 的本地模拟器进行预览&#xff0c;帮助提升项目开发…

央国企信创替代,2027年目标百分达成!信创人才评价成标配?

在2027年之前&#xff0c;央国企实现100%的信创替代&#xff0c;标志着中国信息技术应用创新产业发展步入关键阶段。 这一目标不仅体现了国家对于科技自主可控的高度重视&#xff0c;也预示着国内信创产业将迎来前所未有的发展机遇。 一、政策与市场背景 自2020年以来&#xff…

BOE(京东方)全新一代发光器件赋能iQOO 13 全面引领柔性显示行业性能新高度

10月30日,备受瞩目的iQOO最新旗舰机——被誉为“性能之光”的iQOO 13在深圳震撼发布。该款机型由BOE(京东方)独供6.82英寸超旗舰2K LTPO直屏,行业首发搭载全新一代Q10发光器件,在画面表现、护眼舒适度及性能功耗方面均达到行业领先水准,并以“直屏超窄边”的设计为用户呈现了前…

Python Requests 的高级使用技巧:应对复杂 HTTP 请求场景

介绍 网络爬虫&#xff08;Web Crawler&#xff09;是自动化的数据采集工具&#xff0c;用于从网络上提取所需的数据。然而&#xff0c;随着反爬虫技术的不断进步&#xff0c;很多网站增加了复杂的防护机制&#xff0c;使得数据采集变得更加困难。在这种情况下&#xff0c;Pyt…

【SAP Hana】X-DOC:数据仓库ETL如何抽取SAP中的CDS视图数据

【SAP Hana】X-DOC&#xff1a;数据仓库ETL如何抽取SAP中的CDS视图数据 1、无参CDS对应数据库视图2、有参CDS对应数据库表函数3、封装有参CDS为无参CDS&#xff0c;从而对应数据库视图 1、无参CDS对应数据库视图 select * from ZFCML_REP_V where mandt 300;2、有参CDS对应数…

提升网站速度与性能优化的有效策略与实践

内容概要 在数字化快速发展的今天&#xff0c;网站速度与性能优化显得尤为重要&#xff0c;它直接影响用户的浏览体验。用户在访问网站时&#xff0c;往往希望能够迅速获取信息&#xff0c;若加载时间过长&#xff0c;轻易可能导致他们转向其他更为流畅的网站。因此&#xff0…

C#界面设计--9--fatal error C1083: 无法打开包括文件:“jruparse.h”: No such file or directory

1、VS2008-编译时报错“fatal error C1083: 无法打开包括文件:“jruparse.h”: No such file or directory” 2、问题出现的原因及解决方法 1、如果要引入的这些,h文件跟.cpp在同一个目录下&#xff0c;就不会出现这种问题&#xff0c;检査在工程的include目录下是不是真的存…

‍️CentOS7.9 mall 部署【高可用版本】【本机部署】

文章目录 [TOC]技术选型后端技术前端技术移动端技术开发环境架构图业务架构图 项目部署实操主机规划中间件版本服务规划系统准备开始部署[[#MYSQL]]建立主从关系再次配置成为双主双从为 mysql 集群配置 vip [[#mongodb]]在主节点上无认证登录 [[#redis]]在主节点上查看集群状态…

【数据结构】-数组

数组 特点&#xff1a; 数组的地址连续&#xff0c;可以通过下标获取数据。 1. 数组扩容 步骤&#xff1a; $1. 创建一个比原来数组更长的新数组 $2. 让原来数组当中的数据依次复制到新数组当中 $3. 让arr指向新数组&#xff0c;原数组空间释放 2. 数组插入 2.1 最后位置…

Java 使用Maven Surefire插件批量运行单元测试

在基于Maven的Java项目中可以使用Maven 的 mvn test 命令来运行单元测试。 示例 有一个简单的Maven 项目&#xff0c; pom.xml 只导入了JUnit 5 的相关依赖&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://m…

如何搭建题库管理小序❓

土著刷题小&#x1f34a;序不仅能够作为组织考试的利器&#xff0c;它同样可以帮助教育培训机构构建一个强大且高效的题库管理系统。 下面跟随我们的指导&#xff0c;一起来看看如何利用土著刷题小&#x1f34a;序轻松快捷地建立起自己的题库&#xff0c;并享受其所带来的诸多好…

ComfyUI - ComfyUI 工作流中集成 SAM2 + GroundingDINO 处理图像与视频 教程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/143359538 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 SAM2 与…

汽车免拆诊断案例 | 2010款起亚赛拉图车发动机转速表指针不动

故障现象  一辆2010款起亚赛拉图车&#xff0c;搭载G4ED 发动机&#xff0c;累计行驶里程约为17.2万km。车主反映&#xff0c;车辆行驶正常&#xff0c;但组合仪表上的发动机转速表指针始终不动。 故障诊断  接车后进行路试&#xff0c;车速表、燃油存量表及发动机冷却温度…

MFC界面开发组件Xtreme Toolkit Pro v24全新发布—完整的SVG支持

Codejock软件公司的Xtreme Toolkit Pro是屡获殊荣的VC界面库&#xff0c;是MFC开发中最全面界面控件套包&#xff0c;它提供了Windows开发所需要的11种主流的Visual C MFC控件&#xff0c;包括Command Bars、Controls、Chart Pro、Calendar、Docking Pane、Property Grid、Repo…

基于Springboot+Vue的智慧社区数字治理平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 智慧社区作为智慧城市…

SpringMVC学习中遇到编码问题(过滤器)

一、准备 1、创建一个简单的spring项目&#xff0c;具体不多说&#xff0c;这里主要使用Java配置来搭建。 2、这里我们就只简单的添加两个主要的包&#xff08;若是想要其他的自己添加&#xff09; <dependency><groupId>org.springframework</groupId>&l…

搭建支持国密GmSSL的Nginx环境

准备 1、服务器准备&#xff1a;本文搭建使用的服务器是CentOS 7.6 2、安装包准备&#xff1a;需要GmSSL、国密Nginx&#xff0c;可通过互联网下载或者从 https://download.csdn.net/download/m0_46665077/89936158 下载国密GmSSL安装包和国密Nginx安装包。 服务器安装依赖包…

遥遥领先!手把手带你用国产香橙派部署清华AI语言模型,比肩GPT,树莓派做得到吗?

感谢 顾子韵 &#xff0c;Tass及其他朋友的帮助&#xff0c;缺少他们的帮助无法完成该教程。感兴趣的朋友私聊我或他进群一起学习。 省流中文版本 b站手把手教程&#xff0c;小伙伴们可以直接对着视频进行实践&#xff1a; 1.cd /root 来到root目录 2.apt update &&…

-XSS-

链接 https://github.com/do0dl3/xss-labs 搭建过程非常容易的 搭建好之后&#xff0c;就可以点击图片开始闯关了 第一关--JS弹窗函数alert() 显示payload的长度是4 level1.php?nametest level1.php?nametest1 发现只要改变name的值就显示什么在页面上 没有什么过滤的 …