当前位置: 首页 > article >正文

Android平台如何采集屏幕数据并推送RTMP服务器实现无纸化同屏?

如何获取屏幕数据?

Android平台实现无纸化同屏,屏幕采集是关键,也是源头,Android同屏技术允许将Android设备的屏幕内容实时传输并显示在其他设备上。实现方式主要有基于系统自带功能(如某些Android设备支持的无线投屏到智能电视)以及通过开发自定义应用实现。在自定义应用开发中,通常需要借助Android的MediaProjection API 来获取屏幕内容的图像数据,该API提供了屏幕捕获的能力,通过创建MediaProjectionManager实例,发起屏幕捕获请求,获取到MediaProjection对象后,就可以进一步创建虚拟显示,从而获取屏幕的图像流。

一些小的tips

我们理解的Android平台RTMP同屏,采集到数据后,无非就是实现软、硬编码,然后打包发送到RTMP服务器,播放端拉流播放即可,实际上,几乎每一步操作,都可以考虑精细化的设计和处理,实现期望的高稳定、低延迟和资源占用体验。

先说我们做的Android平台RTMP推送模块的功能设计吧:

Android平台RTMP直播推送SDK

  • 音频编码:AAC/SPEEX;
  • 视频编码:H.264、H.265;
  • 推流协议:RTMP;
  • [音视频]支持纯音频/纯视频/音视频推送;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • 支持RTMP推送 live|record模式设置;
  • 支持前置摄像头镜像设置;
  • 支持软编码、特定机型硬编码;
  • 支持横屏、竖屏推送;
  • 支持Android屏幕采集推送;
  • 支持自建标准RTMP服务器或CDN;
  • 支持断网自动重连、网络状态回调;
  • 支持实时动态水印;
  • 支持实时快照;
  • 支持降噪处理、自动增益控制;
  • 支持外部编码前音视频数据对接;
  • 支持外部编码后音视频数据对接;
  • 支持RTMP扩展H.265(需设备支持H.265特定机型硬编码)和Enhanced RTMP;
  • 支持实时音量调节;
  • 支持扩展录像模块;
  • 支持Unity接口;
  • 支持H.264扩展SEI发送模块;
  • 支持Android 5.1及以上版本。

从数据采集开始,我们就需要考虑用怎样的方式最高效?

数据拿到后,如果分辨率过高,要不要做缩放?

如果屏幕不动,数据帧不回调上来,要不要补帧?

到底是软编码还是硬编码?

走264还是265编码?

是不是可以同时采集摄像头?

如果采集摄像头,能不能用camera2采集?

采集到的camera2数据,如何做数据编码和打包传输?

要不要加动态文字、图片水印?

要不要做音频采集,比如支持麦克风或扬声器采集?亦或二者均采集?

要不要本地录像?

起播慢怎么办?

帧率、码率怎么动态配置?

要不要做实时快照?

要不要支持编码前的其他音视频数据类型对接?

要不要支持编码后的音视频数据对接?

音视频采集有时间偏差怎么办?

要不要支持RTMP HEVC?

如果需要采集Unity camera场景怎么办?

什么都做好了,延迟还是高,到底怎么回事?

单个无纸化会议场景,设备数非常多怎么办。。。

技术实现

做了这么多假设,我们以大牛直播SDK的Android的SmartServicePublisherV2的同屏demo为例,介绍下相关的技术实现细节。

启动APP后,先选择需要采集的分辨率(如果选原始分辨率,系统不做缩放),然后选择“启动媒体投影”,并分别启动音频播放采集、采集麦克风。如果音频播放采集和采集麦克风都打开,可以通过右侧下拉框,推送过程中,音频播放采集和麦克风采集实时切换需要注意的是,Android采集音频播放的audio,音频播放采集是依赖屏幕投影的,屏幕投影关闭后,音频播放也就采不到了。

采集到的屏幕数据,特别是高分屏的话,我们可以缩放后再推送。如果对画质和分辨率要求比较高,可以选择原始分辨率。设备支持硬编码,优先选择H.264硬编,如果是H.265硬编,需要RTMP服务器支持扩展H.265(或Enhanced RTMP)。都选择好后,设置RTMP推送的URL,点开始RTMP推送按钮即可。

废话不多说,上代码,启动媒体服务,进入系统后,我们会自动启动媒体服务:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private void start_media_service() {
	Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
	if (Build.VERSION.SDK_INT >= 26) {
		Log.i(TAG, "startForegroundService");
		startForegroundService(intent);
	} else
		startService(intent);

	bindService(intent, service_connection_, Context.BIND_AUTO_CREATE);
	button_stop_media_service_.setText("停止媒体服务");
}

private void stop_media_service() {
	if (media_engine_callback_ != null)
		media_engine_callback_.reset(null);

	if (media_engine_ != null) {
		media_engine_.unregister_callback(media_engine_callback_);
		media_engine_ = null;
	}

	media_engine_callback_ = null;

	if (media_binder_ != null) {
		media_binder_ = null;
		unbindService(service_connection_);
	}

	Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
	stopService(intent);
	button_stop_media_service_.setText("启动媒体服务");
}

Android 6.0及以上版本,动态获取Audio权限:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private boolean check_record_audio_permission() {
	//6.0及以上版本,动态获取Audio权限
	if (PackageManager.PERMISSION_GRANTED == checkPermission(android.Manifest.permission.RECORD_AUDIO, Process.myPid(), Process.myUid()))
		return true;

	return false;
}

private void request_audio_permission() {
	if (Build.VERSION.SDK_INT < 23)
		return;

	Log.i(TAG, "requestPermissions RECORD_AUDIO");
	ActivityCompat.requestPermissions(this, new String[] {android.Manifest.permission.RECORD_AUDIO}, REQUEST_AUDIO_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
	switch(requestCode){
		case REQUEST_AUDIO_CODE:
			if (grantResults != null && grantResults.length > 0 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
				Log.i(TAG, "RECORD_AUDIO permission has been granted");
			}else {
				Toast.makeText(this, "请开启录音权限!", Toast.LENGTH_SHORT).show();
			}
			break;
	}
}

启动、停止媒体投影实现如下:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonStartMediaProjectionListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_video_capture_running()) {
			media_engine_.stop_audio_playback_capture();
			media_engine_.stop_video_capture();
			resolution_selector_.setEnabled(true);
			button_capture_audio_playback_.setText("采集音频播放");
			button_start_media_projection_.setText("启动媒体投影");
			return;
		}

		Intent capture_intent;
		capture_intent = media_projection_manager_.createScreenCaptureIntent();

		startActivityForResult(capture_intent, REQUEST_MEDIA_PROJECTION);
		Log.i(TAG, "startActivityForResult request media projection");
	}
}

启动媒体投影后,选择“采集音频播放”,如果需要采集麦克风,可以点击“采集麦克风”:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonCaptureAudioPlaybackListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_audio_playback_capture_running()) {
			media_engine_.stop_audio_playback_capture();
			button_capture_audio_playback_.setText("采集音频播放");
			return;
		}

		if (!media_engine_.start_audio_playback_capture(44100, 1))
			Log.e(TAG, "start_audio_playback_capture failed");
		else
			button_capture_audio_playback_.setText("停止音频播放采集");
	}
}

private class ButtonStartAudioRecordListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_audio_record_running()) {
			media_engine_.stop_audio_record();
			button_start_audio_record_.setText("采集麦克风");
			return;
		}

		if (!media_engine_.start_audio_record(44100, 1))
			Log.e(TAG, "start_audio_record failed");
		else
			button_start_audio_record_.setText("停止麦克风");
	}
}

启动、停止RTMP推送:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonRTMPPublisherListener implements OnClickListener {
	@Override
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_rtmp_stream_running()) {
			media_engine_.stop_rtmp_stream();
			button_rtmp_publisher_.setText("开始RTMP推送");
			text_view_rtmp_url_.setText("RTMP URL: ");
			Log.i(TAG, "stop rtmp stream");
			return;
		}

		if (!media_engine_.is_video_capture_running())
			return;

		String rtmp_url;
		if (input_rtmp_url_ != null && input_rtmp_url_.length() > 1) {
			rtmp_url = input_rtmp_url_;
			Log.i(TAG, "start, input rtmp url:" + rtmp_url);
		} else {
			rtmp_url = baseURL + String.valueOf((int) (System.currentTimeMillis() % 1000000));
			Log.i(TAG, "start, generate random url:" + rtmp_url);
		}

		media_engine_.set_fps(fps_);
		media_engine_.set_gop(gop_);
		media_engine_.set_video_encoder_type(video_encoder_type);

		if (!media_engine_.start_rtmp_stream(rtmp_url))
			return;

		button_rtmp_publisher_.setText("停止RTMP推送");
		text_view_rtmp_url_.setText("RTMP URL:" + rtmp_url);
		Log.i(TAG, "RTMP URL:" + rtmp_url);
	}
}

http://www.kler.cn/a/511888.html

相关文章:

  • 要获取本地的公网 IP 地址(curl ifconfig.me)
  • 解决 Error: Invalid or corrupt jarfile day04_studentManager.jar 报错问题
  • xiao esp32 S3播放SD卡wav音频
  • “深入浅出”系列之数通篇:(5)TCP的三次握手和四次挥手
  • 什么是SSL及SSL的工作流程
  • 自定义BeanPostProcessor实现自动注入标注了特定注解的Bean
  • 项目实战--网页五子棋(游戏大厅)(3)
  • 如何使用 Redis 作为高效缓存
  • 在swiper中显示echarts图表,echarts的点击事件无效,图例点击也没有反应
  • Maven 快速上手
  • [2025分类时序异常检测指标R-AUC与VUS]
  • Spring Boot依赖管理:Maven与Gradle实战对比
  • NPM 下载依赖超时:npm ERR! RequestError: connect ETIMEDOUT
  • Tensor 基本操作1 | PyTorch 深度学习实战
  • 【Rust自学】13.9. 使用闭包和迭代器改进IO项目
  • 无监督<视觉-语言>模型中的跨模态对齐
  • vue按照官网设置自动导入后ElMessageBox不生效问题
  • 从零开始:Spring Boot核心概念与架构解析
  • springboot迅捷外卖配送系统
  • STM32CubeIDE使用笔记(一)
  • 【Spring】原型 Bean 被固定
  • 【25】Word:林涵-科普文章❗
  • yum和vim的使用
  • 【Elasticsearch入门到落地】6、索引库的操作
  • Matlab自学笔记四十五:日期时间型和字符、字符串以及double型的相互转换方法
  • React 中hooks之 React useCallback使用方法总结