Android平台如何实现低延迟屏幕采集编码打包并推送RTMP服务或轻量级RTSP服务

技术背景

好多开发者希望搞清楚,Android平台同屏采集到编码推送RTMP的大概流程,简单来说,Android上采集屏幕数据,先进行audio和屏幕采集权限申请,包括在清单文件中声明权限和动态申请权限,然后初始化MediaProjectionManager,创建虚拟显示并开始采集,包括处理采集结果时对图像数据的处理。采集到数据后,通过jni层数据接口,发给底层模块,底层模块完成数据的编码打包传输即可。

技术实现

申请权限

android.permission.RECORD_AUDIO(如果需要采集音频):用于获取设备的音频录制权限,以便在屏幕采集的同时录制系统声音。

android.permission.MEDIA_PROJECTION:这是进行屏幕采集的关键权限,允许应用获取屏幕内容。在应用中,需要通过ActivityResultContracts.StartActivityForResult来请求该权限,系统会弹出一个提示框让用户确认是否允许应用获取屏幕内容。

屏幕采集

初始化相关组件

获取MediaProjectionManager实例,通过getSystemService(Context.MEDIA_PROJECTION_SERVICE)方法来获取系统的屏幕投影管理服务,用于后续的屏幕采集操作。

创建ImageReader对象,ImageReader用于接收屏幕图像数据。使用ImageReader.newInstance(width, height, pixelFormat, maxImages)方法创建,其中width和height是采集的屏幕图像的宽高,pixelFormat是图像的像素格式(如PixelFormat.RGBA_8888等),maxImages表示ImageReader中缓存的图像数量。

设置虚拟显示:使用MediaProjection的createVirtualDisplay方法创建虚拟显示,将屏幕内容投影到ImageReader的Surface上。这样,每当屏幕内容发生变化时,ImageReader就会接收到新的屏幕图像数据。

监听图像数据:通过ImageReader.setOnImageAvailableListener设置监听器,当有新的屏幕图像数据可用时,该监听器的onImageAvailable方法会被调用,在这个方法中可以获取到屏幕图像的Image对象,然后对图像数据进行处理。

编码屏幕数据

选择编码方式:Android 平台上可以使用硬编码或软编码。硬编码通常效率更高,但兼容性可能相对较差;软编码兼容性好,但性能开销较大。一般来说,对于性能要求较高的场景,建议使用硬编码。例如,可以使用MediaCodec进行硬编码,将采集到的屏幕图像数据编码为H.264格式的视频数据。

配置编码参数:根据需要设置编码的参数,如视频的分辨率、帧率、码率等。这些参数会影响编码后的视频质量和带宽占用。

进行编码操作:将从ImageReader获取到的屏幕图像数据输入到MediaCodec中进行编码,编码后的视频数据可以保存在ByteBuffer中。

推送 RTMP 流

选择 RTMP 库:Android 平台上有一些开源的 RTMP 推送库,也可以使用一些第三方的音视频 SDK,这些 SDK 通常已经集成了 RTMP 推送功能,使用起来更加方便。例如,可以使用大牛直播 SDK。

初始化 RTMP 连接:使用选择的 RTMP 库或 SDK,初始化 RTMP 连接,设置 RTMP 服务器的地址、端口号、推流路径等参数。

发送编码后的数据:将编码后的视频数据通过 RTMP 连接发送到服务器。在发送数据时,需要按照 RTMP 协议的格式将数据封装成 RTMP 数据包,然后通过网络发送出去。

处理异常情况

在屏幕采集、编码和推送过程中,可能会出现各种异常情况,如权限申请失败、屏幕采集异常、编码错误、网络连接问题等。需要对这些异常情况进行捕获和处理,以保证应用的稳定性和可靠性。例如,当权限申请失败时,提示用户重新申请权限;当网络连接中断时,尝试重新连接等。

代码示例

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

以Android平台RTMP推送模块为例,我们主要实现了如下功能:

  • 音频编码: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及以上版本。

轻量级RTSP服务,在上述非RTMP协议依赖的基础上,增加了如下功能:

  •  [音频格式]AAC;
  •  [视频格式]H.264、H.265;
  •  [协议类型]RTSP;
  •  [传输模式]支持单播和组播模式;
  •  [端口设置]支持RTSP端口设置;
  •  [鉴权设置]支持RTSP鉴权用户名、密码设置;
  •  [获取session连接数]支持获取当前RTSP服务会话连接数;
  •  [多服务支持]支持同时创建多个内置RTSP服务;
  •  [RTSP url回调]支持设置后的rtsp url通过event回调到上层。

编码的话,考虑到屏幕分辨率一般不会太低,我们可以缩放后再推送,默认我们开启了原始分辨率、标准分辨率、低分辨率选项设置。一般建议标准分辨率即可。如果对画质和分辨率要求比较高,可以选择原始分辨率。设备支持硬编码,优先选择H.264硬编,如果是H.265硬编,需要RTMP服务器支持扩展H.265(或Enhanced RTMP)。

都选择好后,设置RTMP推送的URL,点开始RTMP推送按钮即可。

如果需要通过轻量级RTSP服务,发布RTSP流,先点击启动RTSP服务按钮,RTSP服务启动后,再点击启动RTSP流,RTSP流发布成功后,界面会回调上来RTSP拉流的URL。

下面从代码逻辑实现角度,介绍下同屏的具体流程:

启动媒体服务,进入系统后,我们会自动启动媒体服务,对应的实现逻辑如下:

/*
 * 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");
	}
}

启动、停止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);
	}
}

启动RTSP服务:

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

		if (media_engine_.is_rtsp_server_running()) {
			media_engine_.stop_rtsp_stream();
			media_engine_.stop_rtsp_server();
			button_rtsp_publisher_.setText("启动RTSP流");
			button_rtsp_service_.setText("启动RTSP服务");
			text_view_rtsp_url_.setText("RTSP URL:");
			return;
		}

		if (!media_engine_.start_rtsp_server(rtsp_port_, null, null))
			return;

		button_rtsp_service_.setText("停止RTSP服务");
	}
}

发布RTSP流:

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

		if (media_engine_.is_rtsp_stream_running()) {
			media_engine_.stop_rtsp_stream();
			button_rtsp_publisher_.setText("启动RTSP流");
			text_view_rtsp_url_.setText("RTSP URL:");
			return;
		}

		if (!media_engine_.is_video_capture_running())
			return;

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

		if (!media_engine_.start_rtsp_stream("stream1"))
			return;

		button_rtsp_publisher_.setText("停止RTSP流");
	}
}

RTSP流发布成功后,底层会把RTSP拉流的URL回调上来:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
@Override
public void on_nt_rtsp_stream_url(String url) {
	Log.i(TAG, "on_nt_rtsp_stream_url: " + url);

	MainActivity activity = get_activity();
	if (activity != null) {
		activity.runOnUiThread(new Runnable() {
			MainActivity activity_;
			String url_;

			@Override
			public void run() {
			   activity_.text_view_rtsp_url_.setText("RTSP URL:" + url_);
			}

			public Runnable set(MainActivity activity, String url) {
				this.activity_ = activity;
				this.url_ = url;
				return this;
			}
		}.set(activity, url));
	}
}

可以看到,上述操作,都是在MainActivity.java调用的,如果是需要做demo版本集成,只需要关注MainActivity.java的业务逻辑即可,为了便于开发者对接,我们做了接口的二次封装,除了常规的RTMP推送、轻量级RTSP服务设计外,如果需要录像,只要在MainActivity.java调用这里的接口逻辑即可,非常方便:

/*
 * NTStreamMediaEngine.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
package com.daniulive.smartpublisher;

public interface NTStreamMediaEngine {
    void register_callback(Callback callback);

    void unregister_callback(Callback callback);

    void set_resolution_level(int level);

    int get_resolution_level();

    /*
    * 启动媒体投影
     */
    boolean start_video_capture(int token_code, android.content.Intent token_data);

    boolean is_video_capture_running();

    void stop_video_capture();

    /*
    * 启动麦克风
     */
    boolean start_audio_record(int sample_rate, int channels);

    boolean is_audio_record_running();

    void stop_audio_record();

    /*
     *  Android 10及以上支持, Android10以下设备调用直接返回false
     *  需要有RECORD_AUDIO权限
     *  要开启媒体投影
     */
    boolean start_audio_playback_capture(int sample_rate, int channels);

    boolean is_audio_playback_capture_running();

    void stop_audio_playback_capture();

    /*
     * 输出的音频类型
     *  0: 不输出音频
     *  1: 输出麦克风
     *  2: 输出audio playback(Android 10及以上支持)
     */
    boolean set_audio_output_type(int type);

    int get_audio_output_type();

    void set_fps(int fps);

    void set_gop(int gop);

    boolean set_video_encoder_type(int video_encoder_type);

    int get_video_encoder_type();

    /*
    * 推送RTMP
     */
    boolean start_rtmp_stream(String url);

    boolean is_rtmp_stream_running();

    String get_rtmp_stream_url();

    void stop_rtmp_stream();

    /*
    * 启动RTSP Server, 需要设置端口,用户名和密码可选
     */
    boolean start_rtsp_server(int port, String user_name, String password);

    boolean is_rtsp_server_running();

    void stop_rtsp_server();

    /*
    * 发布RTSP流
     */
    boolean start_rtsp_stream(String stream_name);

    boolean is_rtsp_stream_running();

    String get_rtsp_stream_url();

    void stop_rtsp_stream();

    /*
    * 启动本地录像
     */
    boolean start_stream_record(String record_directory, int file_max_size);

    boolean is_stream_recording();

    void stop_stream_record();

    boolean is_stream_running();

    interface Callback {
        void on_nt_video_capture_stop();
        void on_nt_rtsp_stream_url(String url);
    }
}

如果对音视频这块相对了解的开发者,可以继续到NTStreamMediaProjectionEngineImpl.java文件,查看或修改相关的技术实现:

/*
 * NTStreamMediaProjectionEngineImpl.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
package com.daniulive.smartpublisher;

import android.app.Activity;
import android.app.Application;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.Image;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.WindowManager;
import android.view.WindowMetrics;

import com.eventhandle.NTSmartEventCallbackV2;
import com.eventhandle.NTSmartEventID;
import com.voiceengine.NTAudioRecordV2;
import com.voiceengine.NTAudioRecordV2Callback;
import com.videoengine.NTMediaProjectionCapture;
import com.voiceengine.NTAudioPlaybackCapture;

import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;

public class NTStreamMediaProjectionEngineImpl implements AutoCloseable, NTStreamMediaEngine,
        NTVirtualDisplaySurfaceSinker.Callback, NTMediaProjectionCapture.Callback {
    private static final String TAG = "NTLogProjectionEngine";

    private static final Size DEFAULT_SIZE = new Size(1920, 1080);

    public static final int RESOLUTION_LOW = 0;
    public static final int RESOLUTION_MEDIUM = 1;
    public static final int RESOLUTION_HIGH = 2;

    private final Application application_;

    private final long image_thread_id_;
    private final long running_thread_id_;

    private final Handler image_handler_;
    private final Handler running_handler_;

    private final WindowManager window_manager_;
    private final MediaProjectionManager projection_manager_;
    private int screen_density_dpi_ = android.util.DisplayMetrics.DENSITY_DEFAULT;

    private final SmartPublisherJniV2 lib_publisher_;
    private final LibPublisherWrapper.RTSPServer rtsp_server_;
    private final LibPublisherWrapper stream_publisher_;

    private final CopyOnWriteArrayList<NTStreamMediaEngine.Callback> callbacks_ = new CopyOnWriteArrayList<>();

    private final AtomicReference<VideoSinkerCapturePair> video_capture_pair_ = new AtomicReference<>();

    private final AudioRecordCallbackImpl audio_record_callback_;
    private final AudioPlaybackCaptureCallbackImpl audio_playback_capture_callback_;

    private final AtomicReference<NTAudioRecordV2> audio_record_ = new AtomicReference<>();
    private final AtomicReference<NTAudioPlaybackCapture> audio_playback_capture_ = new AtomicReference<>();
	
	...
}

总结

Android平台实现屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务整体流程非常清晰,所以说,如果只是技术学习,很容易打通并实现相关推流demo,如果是要实现高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器,整体部署,内网大并发环境下,还需要考虑到如何组网等诸多因素。感兴趣的开发者,可以单独跟我沟通探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值