Android平台RTMP直播推送模块技术接入说明

270 阅读5分钟

技术背景

大牛直播SDK跨平台RTMP直播推送模块,始于2015年,支持Windows、Linux(x64_64架构|aarch64)、Android、iOS平台,支持采集推送摄像头、屏幕、麦克风、扬声器、编码前、编码后数据对接,功能强大,性能优异,配合大牛直播SDK的SmartPlayer播放器,轻松实现毫秒级的延迟体验,满足大多数行业的使用场景。

RTMP直播推送模块数据源,支持编码前、编码后数据对接:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据)。

技术对接

系统要求

  • SDK支持Android5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);

  • smartavengine.jar加入到工程;

  • 拷贝libSmartPublisher.so到工程;

  • AndroidManifast.xml添加相关权限:

  • Load相关so:

    static {
    System.loadLibrary("SmartPublisher"); }

  • build.gradle配置32/64位库:

    splits { abi { enable true reset() // Specifies a list of ABIs that Gradle should create APKs for include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for // Specify that we do not want to also generate a universal APK that includes all ABIs universalApk true } }

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;

  • 如何改app-name,strings.xml做以下修改:

    SmartPublisherSDKDemo

接口设计

功能支持

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

接口调用详解

本文以大牛直播SDK Android平台Camera2Demo为例,推送RTMP之前,可以先选择视频分辨率、软编还是硬编码,音频是AAC、SPEEX还是PCMA编码等基础设置,其他参数的设置,可以参考下面InitAndSetConfig()。

以Android平台Camera2对接为例,onCreate()时,想new SmartPublisherJniV2():

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	
	...

	context_ = this.getApplicationContext();
	
	libPublisher = new SmartPublisherJniV2();
}

推送RTMP:

class ButtonStartPushListener implements View.OnClickListener {
	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.101:1935/hls/stream123";;


		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();
		startLayerPostThread();

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

	}
}

stopPush()实现如下:

//停止rtmp推送
private void stopPush() {
	stream_publisher_.StopPublisher();
	stream_publisher_.try_release();

	if (!stream_publisher_.is_publishing())
		stopAudioRecorder();
}

其中,InitAndSetConfig()实现如下,通过调SmartPublisherOpen()接口,生成推送实例句柄。

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

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

	int audio_opt = 1;
	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3,  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);
}

对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
	if (null == lib_publisher) {
		Log.e(TAG, "initialize_publisher lib_publisher is null");
		return false;
	}

	if (0 == handle) {
		Log.e(TAG, "initialize_publisher handle is 0");
		return false;
	}

	if (videoEncodeType == 1) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
		Log.i(TAG, "h264HWKbps: " + kbps);
		int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
		if (isSupportH264HWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High

			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4
			lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
			//lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2

			// lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);

			Log.i(TAG, "Great, it supports h.264 hardware encoder!");
		}
	} else if (videoEncodeType == 2) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
		Log.i(TAG, "hevcHWKbps: " + kbps);
		int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
		if (isSupportHevcHWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);

			// libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);

			Log.i(TAG, "Great, it supports hevc hardware encoder!");
		}
	}

	boolean is_sw_vbr_mode = true;
	//H.264 software encoder
	if (is_sw_vbr_mode) {
		int is_enable_vbr = 1;
		int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
		int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
		lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
	}

	if (is_pcma_) {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
	} else {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
	}

	lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));

	lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);

	lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);

	lib_publisher.SmartPublisherSetGopInterval(handle, gop);

	lib_publisher.SmartPublisherSetFPS(handle, fps);

	// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);

	boolean is_noise_suppression = true;
	lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);

	boolean is_agc = false;
	lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);

	int echo_cancel_delay = 0;
	lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);

	return true;
}

数据投递如下(以Camera2采集为例,如果是其他视频格式,也可以正常对接):

@Override
public void onCameraImageData(Image image) {
	....
	for (LibPublisherWrapper i : publisher_array_)
		i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
			planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
			planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
			planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(),
			w, h, 0, 0,
			scale_w, scale_h, scale_filter_mode, rotation_degree);

}

音频采集投递设计如下:

void startAudioRecorder() {
	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数据的地方,直接投递出去:

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

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

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

		return null;
	}

	@Override
	public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int 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);
	}
}

图层投递设计如下,图层投递的时候,可设置是否添加文字、图片动态水印:

private void startLayerPostThread() {
	if (layer_post_thread_ != null)
		return;

	layer_post_thread_ = new LayerPostThread(this.context_, publisher_array_);
	layer_post_thread_.start_post();
	update_layer_post_video_size();
	layer_post_thread_.enableText(isHasTextWatermark());
	layer_post_thread_.enablePicture(isHasPictureWatermark());
}

private void update_layer_post_video_size() {
	if (null == layer_post_thread_)
		return;

	int w, h;
	int degree = cameraImageRotationDegree_;
	if (degree < 0 ) {
		w = 0;
		h = 0;
	} else if (90 == degree || 270 == degree) {
		w = video_height_;
		h = video_width_;
	}else {
		w = video_width_;
		h = video_height_;
	}

	layer_post_thread_.update_video_size(w, h);
}

private void stopLayerPostThread() {
	if (layer_post_thread_ != null) {
		layer_post_thread_.stop_post();
		layer_post_thread_ = null;
	}
}

如需摄像头快照,调用以下逻辑实现即可:

class ButtonCaptureImageListener implements View.OnClickListener {
	public void onClick(View v) {
		if (null == snap_shot_impl_) {
			snap_shot_impl_ = new SnapShotImpl(image_path_, context_, handler_, libPublisher, snap_shot_publisher_);
			snap_shot_impl_.start();
		}

		startLayerPostThread();
		snap_shot_impl_.set_layer_post_thread(layer_post_thread_);

		snap_shot_impl_.capture();
	}
}

如需集成录像模块,开始录像、停止录像设计如下:

class ButtonStartRecorderListener implements View.OnClickListener {
	public void onClick(View v) {
		if (layer_post_thread_ != null)
			layer_post_thread_.update_layers();

		if (stream_publisher_.is_recording()) {
			stopRecorder();

			if (stream_publisher_.empty())
				ConfigControlEnable(true);

			btnStartRecorder.setText("实时录像");
			btnPauseRecorder.setText("暂停录像");
			btnPauseRecorder.setEnabled(false);
			isPauseRecording = true;
			return;
		}

		Log.i(TAG, "onClick start recorder..");

		InitAndSetConfig();

		ConfigRecorderParam();

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

		startAudioRecorder();
		ConfigControlEnable(false);

		startLayerPostThread();

		btnStartRecorder.setText("停止录像");
		btnPauseRecorder.setEnabled(true);
		isPauseRecording = true;
	}
}

录像参数配置实现如下:

void ConfigRecorderParam() {
	if (null == libPublisher)
		return;

	if (null == recDir || recDir.isEmpty())
		return;

	int ret = libPublisher.SmartPublisherCreateFileDirectory(recDir);
	if (ret != 0) {
		Log.e(TAG, "Create record dir failed, path:" + recDir);
		return;
	}

	if (!stream_publisher_.SetRecorderDirectory(recDir)) {
		Log.e(TAG, "Set record dir failed , path:" + recDir);
		return;
	}

	// 更细粒度控制录像的, 一般情况无需调用
	//libPublisher.SmartPublisherSetRecorderAudio(publisherHandle, 0);
	//libPublisher.SmartPublisherSetRecorderVideo(publisherHandle, 0);

	if (!stream_publisher_.SetRecorderFileMaxSize(200)) {
		Log.e(TAG, "SmartPublisherSetRecorderFileMaxSize failed.");
		return;
	}
}

暂停录像、恢复录像设计如下:

class ButtonPauseRecorderListener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_publisher_.is_recording()) {
			if (isPauseRecording) {
				boolean ret = stream_publisher_.PauseRecorder(true);
				if (ret) {
					isPauseRecording = false;
					btnPauseRecorder.setText("恢复录像");
				} else {
					Log.e(TAG, "Pause recorder failed..");
				}
			} else {
				boolean ret = stream_publisher_.PauseRecorder(false);
				if (ret) {
					isPauseRecording = true;
					btnPauseRecorder.setText("暂停录像");
				} else {
					Log.e(TAG, "Resume recorder failed..");
				}
			}
		}
	}
}

Event回调实现如下:

private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {

		Log.i(TAG, "EventHandeV2: handle=" + handle + " id:" + id);

		String publisher_event = "";

		switch (id) {
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STARTED:
				publisher_event = "开始..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
				publisher_event = "连接中..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED:
				publisher_event = "连接失败..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
				publisher_event = "连接成功..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
				publisher_event = "连接断开..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
				publisher_event = "关闭..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
				publisher_event = "开始一个新的录像文件 : " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
				if (record_executor_ != null) {
					RecordExecutorService executor = record_executor_.get();
					if (executor != null) {
						RecordFileFinishedHandler file_finished_handler = new RecordFileFinishedHandler().set(handle, param3, param1);
						if (param2 > 0)
							file_finished_handler.set_begin_time(param2);

						executor.execute(file_finished_handler);
					}
				}
				publisher_event = "已生成一个录像文件 : " + param3;
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
				publisher_event = "发送时延: " + param1 + " 帧数:" + param2;
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE:
				publisher_event = "快照: " + param1 + " 路径:" + param3;
				if (0 == param1)
					publisher_event = publisher_event + "截取快照成功.." + ", 用户数据:" + param4;
				 else
					publisher_event = publisher_event + "截取快照失败..";

				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
				publisher_event = "RTSP服务URL: " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_RESPONSE_STATUS_CODE:
				publisher_event ="RTSP status code received, codeID: " + param1 + ", RTSP URL: " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_NOT_SUPPORT:
				publisher_event ="服务器不支持RTSP推送, 推送的RTSP URL: " + param3;
				break;
		}

		String str = "当前回调状态:" + publisher_event;

		Log.i(TAG, str);

		if (handler_ != null) {
			android.os.Handler handler = handler_.get();
			if (handler != null) {
				Message message = new Message();
				message.what = PUBLISHER_EVENT_MSG;
				message.obj = publisher_event;
				handler.sendMessage(message);
			}
		}
	}

	public NTSmartEventCallbackV2 set(android.os.Handler handler, RecordExecutorService record_executor) {
		this.handler_ = new WeakReference<>(handler);
		this.record_executor_ = new WeakReference<>(record_executor);
		return this;
	}

	private WeakReference<android.os.Handler> handler_;
	private WeakReference<RecordExecutorService> record_executor_;
}

onDestroy() 的时候,调用stopPush()即可,如果有录像和快照,都停掉,此外,停掉图层投递线程,并关闭camera:

@Override
protected void onDestroy() {
	Log.i(TAG, "activity destory!");

	record_executor_.cancel_tasks();

	stopAudioRecorder();

	if (snap_shot_impl_ != null) {
		snap_shot_impl_.stop();
		snap_shot_impl_ = null;
	}

	snap_shot_publisher_.release();

	stopPush();
	stopRecorder();

	stream_publisher_.release();

	stopLayerPostThread();

	if (camera2Helper != null) {
		camera2Helper.release();
	}

	if (!record_executor_.shutdown(60, TimeUnit.SECONDS))
		Log.w(TAG, "call record_executor_.shutdown failed");

	super.onDestroy();
}

总结

以上是大牛直播SDK的Android平台RTMP直播推送模块详细的对接说明,除了可以对接编码前各种类型的音视频数据外,模块还支持对接编码后音视频数据,并实现本地录像、快照等功能,除支持H.264外,RTMP推送模块还支持扩展H.265和Enhanced RTMP。感兴趣的开发者,可以单独跟我们探讨。