技术背景
我们在对接GB28181设备接入侧的时候,除了常规实时音视频按需上传外,还有个重要的功能,就是本地实时录像,录像后的数据,在执法记录仪等前端设备留底,然后,到工作站拷贝到专门的平台。
本文探讨的是,基于GB28181设备接入更进一步的处理:录像查询和录像下载,本文以我们Android平台开发的GB28181设备接入为例,做个简单的分析。
本地录像存储
GB28181设备接入侧,非常重要的功能属性就是实时录像,我们在做实时录像的时候,设计如下:
先说录像参数设置:
/**
* SmartPublisherJniV2.java
* Author: daniusdk.com
* Created on 2015/09/20.
*/
/**
* 音频录制开关, 目的是为了更细粒度的去控制录像, 一般不需要调用这个接口, 这个接口使用场景比如同时推送音视频,但只想录制视频,可以调用这个接口关闭音频录制
*
* @param is_recoder: 0: do not recorder; 1: recorder; sdk默认是1
*
* @return {0} if successful
*/
public native int SmartPublisherSetRecorderAudio(long handle, int is_recoder);
/**
* 视频录制开关, 目的是为了更细粒度的去控制录像, 一般不需要调用这个接口, 这个接口使用场景比如同时推送音视频,但只想录制音频,可以调用这个接口关闭视频录制
*
* @param is_recoder: 0: do not recorder; 1: recorder; sdk默认是1
*
* @return {0} if successful
*/
public native int SmartPublisherSetRecorderVideo(long handle, int is_recoder);
/**
* Create file directory(创建录像存放目录)
*
* @param path, E.g: /sdcard/daniulive/rec
*
* <pre> The interface is only used for recording the stream data to local side. </pre>
*
* @return {0} if successful
*/
public native int SmartPublisherCreateFileDirectory(String path);
/**
* Set recorder directory(设置录像存放目录)
*
* @param path: the directory of recorder file.
*
* <pre> NOTE: make sure the path should be existed, or else the setting failed. </pre>
*
* @return {0} if successful
*/
public native int SmartPublisherSetRecorderDirectory(long handle, String path);
/**
* Set the size of every recorded file(设置单个录像文件大小,如超过最大文件大小,自动切换到下个文件录制)
*
* @param size: (MB), (5M~500M), if not in this range, set default size with 200MB.
*
* @return {0} if successful
*/
public native int SmartPublisherSetRecorderFileMaxSize(long handle, int size);
录像控制:
/**
* Start recorder(开始录像)
*
* @return {0} if successful
*/
public native int SmartPublisherStartRecorder(long handle);
/**
* Pause recorder(暂停/恢复录像)
*
* is_pause: 1表示暂停, 0表示恢复录像, 输入其他值将调用失败
*
* @return {0} if successful
*/
public native int SmartPublisherPauseRecorder(long handle, int is_pause);
/**
* Stop recorder(停止录像)
*
* @return {0} if successful
*/
public native int SmartPublisherStopRecorder(long handle);
录像状态回调
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)
executor.execute(new RecordFileFinishedHandler().set(handle, param3, param1));
}
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 (param1 == 0) {
publisher_event = publisher_event + "截取快照成功..";
} 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_;
}
为适配GB28181的录像查询和处理,我们会把录像的文件,文件名做一定的处理,比如加上开始、结束时间还有duration和file size。
录像调用逻辑如下:
void ConfigRecorderParam() {
if (libPublisher != null && publisherHandle != 0) {
if (recDir != null && !recDir.isEmpty()) {
int ret = libPublisher.SmartPublisherCreateFileDirectory(recDir);
if (0 == ret) {
if (0 != libPublisher.SmartPublisherSetRecorderDirectory(publisherHandle, recDir)) {
Log.e(TAG, "Set record dir failed , path:" + recDir);
return;
}
// 更细粒度控制录像的, 一般情况无需调用
//libPublisher.SmartPublisherSetRecorderAudio(publisherHandle, 0);
// libPublisher.SmartPublisherSetRecorderVideo(publisherHandle, 0);
if (0 != libPublisher.SmartPublisherSetRecorderFileMaxSize(publisherHandle, 200)) {
Log.e(TAG, "SmartPublisherSetRecorderFileMaxSize failed.");
return;
}
} else {
Log.e(TAG, "Create record dir failed, path:" + recDir);
}
}
}
}
class ButtonStartRecorderListener implements View.OnClickListener {
public void onClick(View v) {
if (isRecording) {
stopRecorder();
if (!isPushingRtmp && !isRTSPPublisherRunning && !isGB28181StreamRunning) {
ConfigControlEnable(true);
}
btnStartRecorder.setText("实时录像");
btnPauseRecorder.setText("暂停录像");
btnPauseRecorder.setEnabled(false);
isPauseRecording = true;
return;
}
Log.i(TAG, "onClick start recorder..");
if (libPublisher == null)
return;
if (!isPushingRtmp && !isRTSPPublisherRunning&& !isGB28181StreamRunning) {
InitAndSetConfig();
}
ConfigRecorderParam();
int startRet = libPublisher.SmartPublisherStartRecorder(publisherHandle);
if (startRet != 0) {
if (!isPushingRtmp && !isRTSPPublisherRunning && !isGB28181StreamRunning) {
if (publisherHandle != 0) {
long handle = publisherHandle;
publisherHandle = 0;
libPublisher.SmartPublisherClose(handle);
}
}
Log.e(TAG, "Failed to start recorder.");
return;
}
if (!isPushingRtmp && !isRTSPPublisherRunning && !isGB28181StreamRunning) {
CheckInitAudioRecorder();
ConfigControlEnable(false);
}
startLayerPostThread();
btnStartRecorder.setText("停止录像");
isRecording = true;
btnPauseRecorder.setEnabled(true);
isPauseRecording = true;
}
}
class ButtonPauseRecorderListener implements View.OnClickListener {
public void onClick(View v) {
if (isRecording) {
if(isPauseRecording)
{
int ret = libPublisher.SmartPublisherPauseRecorder(publisherHandle, 1);
if (ret == 0)
{
isPauseRecording = false;
btnPauseRecorder.setText("恢复录像");
}
else if(ret == 3)
{
Log.e(TAG, "Pause recorder failed, please re-try again..");
}
else
{
Log.e(TAG, "Pause recorder failed..");
}
}
else
{
int ret = libPublisher.SmartPublisherPauseRecorder(publisherHandle, 0);
if (ret == 0)
{
isPauseRecording = true;
btnPauseRecorder.setText("暂停录像");
}
else if(ret == 3)
{
Log.e(TAG, "Resume recorder failed, please re-try again..");
}
else
{
Log.e(TAG, "Resume recorder failed..");
}
}
}
}
}
总结
如果需要实现GB28181平台的录像查询和录像下载,实时录像的处理必不可少。下一章节,我们将根据GB28181规范探讨录像查询和录像下载。