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

技术背景

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推送的模块,一定要足够的灵活,扩展性好,才能很快的实现客户的技术诉求。以上抛砖引玉,感兴趣的开发者,可以跟我单独探讨。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【十九周】文献阅读:图像识别的深度残差学习

目录 摘要Abstract图像识别的深度残差学习研究背景研究动机解决办法Residual LearningShortcut Connections网络结构 实验结果代码实践论文原文总结 摘要 在之前对神经网络的基础学习中&#xff0c;师兄推荐了我去了解一下 ResNet。因此本周对 ResNet 的开山之作—Deep Residu…

MATLAB/Simulink学习|在Simulink中调用C语言-01使用C Function 实现比例运算

前面的博客中&#xff0c;提到如果想将Simulink仿真推进至硬件实验&#xff0c;需要将积木式的仿真搭建&#xff0c;变换成C语言实现&#xff0c;那么如何在Simulink中验证C代码的正确性呢&#xff1f;我将一边学习&#xff0c;一边更新&#xff0c;一边比较不同方法实现C语言&…

基于BP神经网络的手写体数字图像识别

基于BP神经网络的手写体数字图像识别 摘要 在信息化飞速发展的时代&#xff0c;光学字符识别是一个重要的信息录入与信息转化的手段&#xff0c;其中手写体数字的识别有着广泛地应用&#xff0c;如&#xff1a;邮政编码、统计报表、银行票据等等&#xff0c;因其广泛地应用范围…

分享一个免费的网页转EXE的工具

HTML2EXE是一款在Windows系统下将Web项目或网站打包成EXE执行程序的免费工具。这款工具能够将单页面应用、传统HTMLJavaScriptCSS生成的网站、Web客户端&#xff0c;以及通过现代前端框架&#xff08;如Vue&#xff09;生成的应用转换成独立的EXE程序运行。它支持将任何网站打包…

ubuntu安装与配置Nginx(2)

1. 配置 Nginx Nginx 的配置文件通常位于 /etc/nginx/nginx.conf&#xff0c;而虚拟主机的配置文件通常在 /etc/nginx/sites-available/ 和 /etc/nginx/sites-enabled/ 目录中。 在/etc/nginx/conf.d目录下新建xx.conf文件&#xff0c;配置文件&#xff0c; nginx -t 检查语法…

C++_day2

目录 1. 引用 reference&#xff08;重点&#xff09; 1.1 基础使用 1.2 特性 1.3 引用参数 2. C窄化&#xff08;了解&#xff09; 3. 输入&#xff08;熟悉&#xff09; 4. string 字符串类&#xff08;掌握&#xff09; 4.1 基础使用 4.2 取出元素 4.3 字符串与数字转换 5. …

JAVA WEB — HTML CSS 入门学习

本文为JAVAWEB 关于HTML 的基础学习 一 概述 HTML 超文本标记语言 超文本 超越文本的限制 比普通文本更强大 除了文字信息 还可以存储图片 音频 视频等标记语言 由标签构成的语言HTML标签都是预定义的 HTML直接在浏览器中运行 在浏览器解析 CSS 是一种用来表现HTML或XML等文…

第十五章 Vue工程化开发及Vue CLI脚手架

目录 一、引言 二、Vue CLI 基本介绍 三、安装Vue CLI 3.1. 安装npm和yarn 3.2. 安装Vue CLI 3.3. 查看 Vue 版本 四、创建启动工程 4.1. 创建项目架子 4.2. 启动工程 五、脚手架目录文件介绍 六、核心文件讲解 6.1. index.html 6.2. main.js 6.3. App.vue 一、…

【1个月速成Java】基于Android平台开发个人记账app学习日记——第4天,注册登录功能设计

24.11.03 1.修改项目目录 从今天开始将正式进行功能的设计&#xff0c;首先需要对原来的项目结构进行修改&#xff0c;主要是添加新的文件夹用于存放新的文件。下面进行展示和讲解&#xff1a; 我用红圈圈出了新添加的文件夹&#xff0c;介绍下它们都是干啥的&#xff1a; da…

动态库实现lua网络请求GET, POST, 下载文件

DLL需要使用的网络封装 WinHttp异步实现GET, POST, 多线程下载文件_webclient post下载文件-CSDN博客文章浏览阅读726次。基于WinHttp封装, 实现异步多线程文件下载, GET请求, POST请求_webclient post下载文件https://blog.csdn.net/Flame_Cyclone/article/details/142644088…

unet中的attn_processor的修改(用于设计新的注意力模块)

参考资料 文章目录 unet中的一些变量的数据情况attn_processorunet.configunet_sd 自己定义自己的attn Processor &#xff0c;对原始的attn Processor进行修改 IP-adapter中设置attn的方法 参考的代码&#xff1a; 腾讯ailabipadapter 的官方训练代码 unet中的一些变量的数据…

深度学习基础—序列采样

引言 深度学习基础—循环神经网络&#xff08;RNN&#xff09;https://blog.csdn.net/sniper_fandc/article/details/143417972?fromshareblogdetail&sharetypeblogdetail&sharerId143417972&sharereferPC&sharesourcesniper_fandc&sharefromfrom_link …

Qt中的Model与View5: QStyledItemDelegate

目录 QStyledItemDelegate API 重载公共函数 保护函数 重载保护函数 当在 Qt 项目视图中显示模型数据时&#xff0c;例如 QTableView&#xff0c;每个项目由代理绘制。此外&#xff0c;当项目被编辑时&#xff0c;提供一个编辑器小部件&#xff0c;该小部件在编辑时显示在项…

AI打造超写实虚拟人物:是科技奇迹还是伦理挑战?

内容概要 在这个科技飞速发展的时代&#xff0c;超写实虚拟人物仿佛从科幻小说中走进了我们的日常生活。它们以生动的形象和细腻的动作&#xff0c;不仅在影视、广告和游戏中吸引了无数目光&#xff0c;更让我们对AI技术的未来充满了期待和疑惑。这些数字化身在逼真的外貌下&a…

海浪中的记忆:海滨学院班级回忆录开发

3系统分析 3.1可行性分析 通过对本海滨学院班级回忆录实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本海滨学院班级回忆录采用SSM框架&#xff0c;JAVA作为开…

文本串的应用(1)

一、文本串的加密解密操作 一个文本串可用事先给定的字母映射表进行加密。 例如&#xff0c;假设字母映射表为&#xff1a; a b c d e f g h i j k l m n o p q r s t u v w x y z n g z q t c o b m u h e l k p d a w x f y i v r s j 则字符串“encrypt”被加密为“tkz…

MCU裸机任务调度架构

第1章 方式一(平均主义) int main(int argc, char **argv){/* RTC 初始化 */bsp_RTC_Init(&rtc);/* 串口初始化 */uartInit(115200);/* LED初始化 */ledInit();while(1){// 任务1(获取传感器数据)// 任务2// 任务3} } 1.1 平均主义的缺陷 获取传感器数据可以600ms去读取一…

【力扣专题栏】面试题 01.02. 判定是否互为字符重排,如何利用数组模拟哈希表解决两字符串互排问题?

题解目录 1、题目描述解释2、算法原理解析3、代码编写(1)、两个数组分别模拟哈希表解决(2)、利用一个数组模拟哈希表解决问题 1、题目描述解释 2、算法原理解析 3、代码编写 (1)、两个数组分别模拟哈希表解决 class Solution { public:bool CheckPermutation(string s1, stri…

【OJ题解】C++实现反转字符串中的每个单词

&#x1f4b5;个人主页: 起名字真南 &#x1f4b5;个人专栏:【数据结构初阶】 【C语言】 【C】 【OJ题解】 题目要求&#xff1a;给定一个字符串 s &#xff0c;你需要反转字符串中每个单词的字符顺序&#xff0c;同时仍保留空格和单词的初始顺序。 题目链接: 反转字符串中的所…

全新更新!Fastreport.NET 2025.1版本发布,提升报告开发体验

在.NET 2025.1版本中&#xff0c;我们带来了巨大的期待功能&#xff0c;进一步简化了报告模板的开发过程。新功能包括通过添加链接报告页面、异步报告准备、HTML段落旋转、代码文本编辑器中的文本搜索、WebReport图像导出等&#xff0c;大幅提升用户体验。 FastReport .NET 是…