技术背景

我们知道,Android平台不管RTMP推送、轻量级RTSP服务模块还是GB28181设备接入模块,早期,如果需要实现截图功能,又不想依赖Android系统接口,最好的办法是,在底层实现快照截图。

快照截图,实际上我们2016年就支持了,不过,需要在RTMP推送、轻量级RTSP服务发布RTSP流、开启实时录像或GB28181设备接入侧已经在传数据的时候,有数据下去,才可以实现截图快照。

本次,我们要实现的是,上述条件不满足的情况下,如何让大牛直播SDK的底层模块(libSmartPublisher.so)实时截图。

技术实现

本文以大牛直播SDK的Camera2Demo为例,废话不多说,上方案:

Android平台如何不推RTMP|不发布RTSP流|不实时录像|不回传GB28181数据时实时快照?_大牛直播SDK

这里,我们专门封装了 SnapShotImpl.java

/*
 * SnapShotImpl.java
 * Author: daniusdk.com
 * WeChat: xinsheng120
 */
public SnapShotImpl(String dir, Context context, android.os.Handler handler,
					SmartPublisherJniV2 lib_sdk, LibPublisherWrapper publisher) {
	this.dir_ = dir;

	if (context != null)
		this.context_ = new WeakReference<>(context);

	if (handler != null)
		this.os_handler_ = new WeakReference<>(handler);

	this.lib_sdk_ = lib_sdk;
	this.publisher_ = publisher;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

init_sdk_instance()实现:

protected boolean init_sdk_instance() {
	if (!publisher_.empty())
		return true;

	Context context = application_context();
	if (null == context)
		return false;

	if (null == lib_sdk_)
		return false;

	long handle = lib_sdk_.SmartPublisherOpen(context, 0, 3, 1920, 1080);
	if (0 == handle) {
		Log.e(TAG, "init_sdk_instance sdk open failed!");
		return false;
	}

	lib_sdk_.SetSmartPublisherEventCallbackV2(handle, new SDKEventHandler(this));
	lib_sdk_.SmartPublisherSetFPS(handle, 4);
	publisher_.set(lib_sdk_, handle);
	update_sdk_instance_release_time();

	Log.i(TAG, "init_sdk_instance ok handle:" + handle);
	return true;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

capture()实现:

protected boolean capture(String file_name, boolean is_update_layers, String user_data) {
	if (is_null_or_empty(file_name)) {
		Log.e(TAG, "capture file name is null");
		return false;
	}

	if (publisher_.empty()) {
		if (!init_sdk_instance()) {
			Log.e(TAG, "init_sdk_instance failed");
			return false;
		}
	}

	if (is_update_layers && layer_post_thread_ != null)
		layer_post_thread_.update_layers();

	boolean ret = publisher_.CaptureImage(0, 100, file_name, user_data);
	if (ret)
		update_sdk_instance_release_time();

	return ret;
}

public boolean capture() {
	if (is_null_or_empty(dir_)) {
		Log.e(TAG, "capture dir is null");
		return false;
	}

	String file_name = dir_ + File.separatorChar +  make_current_date_time_string() + JPEG_SUFFIX;
	boolean ret = capture(file_name, true, null);
	Log.i(TAG, "capture ret:" + ret + ", file:" + file_name);
	return  ret;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

对应的CaptureImge()封装如下:

public boolean CaptureImage(int compress_format, int quality, String file_name, String user_data_string) {
	if (!check_native_handle())
		return false;

	return OK == lib_publisher_.CaptureImage(get(), compress_format, quality, file_name, user_data_string);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

模块头文件SmartPublisherJniV2.java接口设计如下:

/**
 * 新的截图接口, 支持JPEG和PNG两种格式
 * @param compress_format: 压缩格式, 0:JPEG格式, 1:PNG格式, 其他返回错误
 * @param quality: 取值范围:[0, 100], 值越大图像质量越好, 仅对JPEG格式有效, 若是PNG格式,请填100
 * @param file_name: 图像文件名, 例如:/dirxxx/test20231113100739.jpeg, /dirxxx/test20231113100739.png
 * @param user_data_string: 用户自定义字符串
 * @return {0} if successful
 */
 public native int CaptureImage(long handle, int compress_format, int quality, String file_name, String user_data_string);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

最外层MainActivity.java调用示例如下:

private final LibPublisherWrapper snap_shot_publisher_ = new LibPublisherWrapper();

private LibPublisherWrapper publisher_array_[] = {stream_publisher_, snap_shot_publisher_};

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();
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

记得投递数据,让有截图快照的数据源传递到底层模块:

@Override
public void onCameraImageData(Image image) {

	Image.Plane[] planes = image.getPlanes();
	...
	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);

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

onDestroy()的时候,记得停掉,并释放资源:

@Override
protected void onDestroy() {
	Log.i(TAG, "activity destory!");
	
	...
	if (snap_shot_impl_ != null) {
		snap_shot_impl_.stop();
		snap_shot_impl_ = null;
	}

	snap_shot_publisher_.release();

	super.onDestroy();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

总结

市面上,咱们能看到的实时截图快照,大多是要么直接基于Android系统接口实现,要么只能在RTMP推送、实时录像、轻量级RTSP服务发布流数据、GB28181设备接入侧回传音视频数据的时候才可以用,如果想要更灵活的处理快照数据,特别是,实现GB/T28181-2022关于快照的技术规范诉求,灵活的快照模式,需要底层模块设计的非常灵活才行,以上是Android平台推送端实时快照的大概设计逻辑,感兴趣的开发者,可以单独跟我沟通讨论。