모션 녹화

모션 녹화는 개별 또는 통합 녹화가 sniffing 모드 상태로 시작하고, 외부 움직임 감지 등의 이벤트로 실제 녹화가 시작됩니다.

녹화 시간은 녹화 객체 생성시 전달한 패러미터(motion-pre-recording-secondsmotion-post-recording-seconds) 값으로 지정된 전후 시간 동안 모션 녹화를 진행합니다.

모션 녹화는 아래 API를 사용합니다.

구분 녹화 시작 녹화 종료
통합 녹화 startMotionRecording stopMotionRecording
개별 녹화 startMultiChannelMotionRecording stopMultiChannelMotionRecording

이벤트 녹화 종료는 stopMotionRecordingstopMultiChannelMotionRecording 를 호출하여 강제로 종료할 수 있습니다. 보통의 경우, 이벤트 녹화가 마치면 자동으로 종료됩니다.

이벤트 녹화 진행 중 발생하는 이벤트 콜백 함수는 아래와 같습니다.

구분 인터페이스 녹화 시작 녹화 종료
통합 녹화 MediaObserver onMotionRecordingStarted onMotionRecordingCompleted
개별 녹화 MediaObserver2 onMotionRecordingStarted onMotionRecordingCompleted

통합 녹화

통합 녹화의 경우, 아래의 startMotionRecording API를 사용합니다.

  • int32_t startMotionRecording(RecorderRef recorder, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size);
    • event_recording_file_path에 응용 프로그램은 이벤트 녹화가 저장될 파일의 절대 경로를 지정합니다. 이벤트 녹화 파일이 위치할 폴더는 녹화 객체 생성시 전달한 패러미터(motion-folder-path) 값으로 지정합니다. 다른 폴더 경로를 사용해도 무방합니다.

개별 녹화

개별 녹화의 경우, 아래의 startMultiChannelMotionRecording API를 사용합니다.

  • int32_t startMultiChannelMotionRecording(MultiChannelRecorderRef multi_channel_recorder, const std::list<MultiChannelRecorderFilePath> & file_paths, void * stream_data_header, size_t stream_data_header_size, bool stop_current_event_recording);
    • file_paths 에 각 채널별 녹화파일 경로를 추가하여 채널 개수 크기의 목록을 만들어 전달합니다.
    • stop_current_event_recordingtrue이면 현재 이벤트 녹화가 진행 중이면 이벤트 녹화를 중지합니다.

개별 녹화의 경우는 채널을 지정하여 모션 녹화를 시작할 수 있습니다.

  • int32_t startMultiChannelMotionRecording(MultiChannelRecorderRef multi_channel_recorder, int32_t channel_id, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size);

예제

아래는 1개의 영상 채널을 sniffing 모드로 녹화를 시작하고, 모션 디텍터로 부터 모션 녹화 이벤트 요청을 받아서 모션 녹화를 시작하는 예입니다.

녹화 객체를 생성합니다. 패러미터는 아래 전체 코드를 참조합니다.

std::shared_ptr<MyRecordingObserver> recording_observer = std::make_shared<MyRecordingObserver>();

RecorderRef recorder = createRecorder(parameters);
if(recorder == nullptr) {
  DLOG(DLOG_ERROR, "recorder creation failed!\r\n");
  oasis::finalize();
  return -1;
}

MyRecordingObserver 에서 Motion 관련 이벤트 콜백 함수를 아래와 같이 로그 출력 용으로 정의합니다.

virtual void onMotionRecordingStarted(void *user_data, media_report_reason_t reason, const char *file_path) {
  DLOG0(DLOG_THIS, "Motion Recording started @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
}

virtual void onMotionRecordingCompleted(void *user_data, media_report_reason_t reason, const char *file_path) {
  DLOG0(DLOG_THIS, "Motion Recording completed @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
}

모션 디텍터를 생성합니다.

std::shared_ptr<MyMotionDetectObserver> motion_detect_observer = std::make_shared<MyMotionDetectObserver>();    

parameters.clear();

parameters["camera-id"] = "0";
parameters["enable"] = "1";
parameters["use-g2d"] = "1";
parameters["buffer-count"] = "3";
parameters["sensitivity-level"] = "5";
parameters["fps"] = "6";
parameters["version"] = "2";
parameters["height-max"] = "135";

MotionDetectorRef motion_detector = createMotionDetector(parameters);
if(motion_detector == nullptr) {
  DLOG(DLOG_ERROR, "motion detector creation failed!\r\n");
  destroyRecorder(recorder);
  oasis::finalize();
  return -1;
}

MyMotionDetectObserver::onMotionDetected 에서 일정 시간(5초) 경과 후 모션 감지된 값이 일정 수치를 넘으면 모션 녹화 명령을 전송합니다.

class MyMotionDetectObserver : public MotionDetectObserver 
{
 public:
  MyMotionDetectObserver() {
    created_timestamp_ = systemTime();
  }
  virtual ~MyMotionDetectObserver() {}

  virtual void onMotionDetected(int32_t camera_id, int32_t detected) {
    TRACE0("camera<%d> motion detected#%d\n", camera_id, detected);
    //after some period of stability passes
    if(systemTime() - created_timestamp_ > 5000000) {
      if(detected > 500) {
        postCommand(kStartMotionRecording);
      }
    }
  }

  uint64_t created_timestamp_;
};

메인 함수에서 모션 디텍터 이벤트 콜백으로 부터 모션 녹화 시작 요청을 처리하기 위해 명령큐 쓰레드를 생성합니다. 모션 녹화 요청을 받으면 모션 녹화를 진행합니다.

t3 = std::thread([&]() {
  RecorderCommand cmd;
  do {
    {
      std::unique_lock<std::mutex> _l(cmdq_lock_);
      while(cmdq_.empty()) {
        cmdq_cond_.wait(_l);
      }
      cmd = cmdq_.front();
      cmdq_.pop_front();
    }

    if(cmd.cmd_ == kRecorderExitNow) {
      DLOG(DLOG_TRACE, "got kRecorderExitNow\r\n");
      break;
    }

    DLOG(DLOG_INFO, "cmd %d\n", cmd.cmd_);

    switch(cmd.cmd_) {
      case kStartMotionRecording: {
        if (recorderIsRunning(recorder) && isMotionRecording(recorder) == false) {
          time_t t;
          struct tm tm_t;
          char path[PATH_MAX];

          time(&t);
          localtime_r(&t, &tm_t);

  #if USE_OFFS
          sprintf(path, "/mnt/sd/EVENT/MTN_%04d-%02d-%02d_%02d-%02d-%02d.mp4", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
  #else
          sprintf(path, "/tmp/EVENT/MTN_%04d-%02d-%02d_%02d-%02d-%02d.mp4", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
  #endif
          startMotionRecording(recorder, path, nullptr, 0);
        } else {
          fprintf(stdout, "Recorder not started (%d) or busy in recording an motion (%d).\n", recorderIsRunning(recorder), isMotionRecording(recorder));
        }
        break;
      }
    default:
        break;
    }

  } while(1);

  {
    std::lock_guard<std::mutex> _l(cmdq_lock_);
    cmdq_.clear();
  }
});

녹화 객체를 sniffing 모드로 시작합니다.

//RUN as SNIFFING mode!!!
err = startRecording(recorder, recording_observer, nullptr, true, &strd_data_header_, sizeof(strd_data_header_));
if(err < 0) {
  DLOG(DLOG_ERROR, "start recording failed\r\n");
  goto done;
}  

모션 디텍터를 시작합니다.

err = motionDetectorStart(motion_detector, motion_detect_observer);
if(err < 0) {
  DLOG0(DLOG_ERROR, "MotionDetectgor start failed\n");
  goto done;
}

TRACE0("MotionDetector<%d> started\n", motionDetectorId(motion_detector));

모션이 감지되면 모션 녹화가 시작되며 이에 대한 이벤트 콜백 함수에서 아래 예와 같이 로그를 출력합니다.

[4512.331458] Motion Recording started @ "/tmp/EVENT/MTN_2000-06-27_05-17-01.mp4" (reason: 0 NoError).
[4524.231554] Motion Recording completed @ "/tmp/EVENT/MTN_2000-06-27_05-17-01.mp4" (reason: 0 NoError).

전체 코드입니다.

#include "OasisAPI.h"
#include "OasisLog.h"
#include "OasisMedia.h"
#include "OasisFS.h"
#include "OasisUtil.h"
#include "OasisMotion.h"

#include <thread>
#include <mutex>
#include <memory>
#include <condition_variable>

#include <signal.h>

#define DLOG_THIS   0x00010000
#define DLOG_RECORD 0x00020000
#define DLOG_FRAME  0x00040000
#define DLOG_TRACE  0x00080000

#undef DLOG_FLAGS
#define  DLOG_FLAGS (DLOG_FLAGS_DEFAULT|DLOG_RECORD|DLOG_TRACE|DLOG_THIS/*|DLOG_FRAME*/)

#undef DLOG_TAG
#define DLOG_TAG "RECORDER"

#define OFFS_QUEUE_SIZE_KBYTES  (20*1024)
#define OFFS_CACHE_SIZE_KBYTES  (1024)
#define OFFS_WRITE_ALIGNMENT_BYTES  8192
#define MEDIA_CACHE_SIZE_KBYTES (40*1024)

#define USE_OFFS 0

extern "C" int getch(const char *prompt);

using namespace oasis;

static char DrivingEventFolder[PATH_MAX] = { 0, };
static char ParkingEventFolder[PATH_MAX] = { 0, };
static char MotionFolder[PATH_MAX] = { 0, };

static bool timer_running = true;
static bool continue_sniffing = true;

static void bytesToString(size_t size, char *size_string)
{
  if(size >= 1024*1024) {
    sprintf(size_string, "%.2fMB", (double)size/1048576);
  } else if(size >= 1024) {
    sprintf(size_string, "%.2fKB", (double)size/1024);
  } else {
    sprintf(size_string, "%dB", size);
  }
}

enum {
  kStartMotionRecording = 1,
  kRecorderExitNow = 2,
};

struct RecorderCommand {
  int32_t cmd_;
  RecorderCommand() = default;
  RecorderCommand(int32_t cmd) : cmd_(cmd) {}
  RecorderCommand(const RecorderCommand& src) : cmd_(src.cmd_) {}
  RecorderCommand& operator=(const RecorderCommand& src) {
    if(this != &src) {
      cmd_ = src.cmd_;
    }
    return *this;
  }
};


static std::condition_variable cmdq_cond_;
static std::mutex cmdq_lock_;
static std::list<RecorderCommand> cmdq_;


static void postCommand(int32_t cmd) {
  try {
    std::lock_guard<std::mutex> _l(cmdq_lock_);
    cmdq_.emplace_back(cmd);
    cmdq_cond_.notify_one();
  } catch(...) {

  }
}

static uint8_t strd_data_header_[24] = {0, };

class MyRecordingObserver : public MediaObserver {
  public:
  virtual void onStarted(void* user_data, const char* file_path) {
    DLOG(DLOG_RECORD|DLOG_INFO, "Recording started, file path \"%s\"\r\n", file_path);
  }

  virtual void onStopped(void* user_data, media_report_reason_t reason, void* details) {
    DLOG(DLOG_RECORD|DLOG_INFO, "Recording stopped, reason: %d(%s)\r\n", reason, mediaReportReasonString(reason));
    if(reason != kMediaReportNoError) {
      continue_sniffing = false;
    }
  }

  virtual void onFileChanged(void* user_data, const char* new_file_path) {
    DLOG(DLOG_RECORD|DLOG_INFO, "New file used: \"%s\"\r\n", new_file_path);
  }

  virtual void onError(void* user_data, media_report_reason_t reason, void* details) {
    if(reason == kRecordingErrorMediaDataTimeout) {
      int32_t camera_id = (intptr_t)details;
      DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s), camera<%d>\r\n", reason, mediaReportReasonString(reason), camera_id);
    } else if(reason == kRecordingErrorMediaCacheReadErrorBegin) {
      DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s) ===> stopping\r\n", reason, mediaReportReasonString(reason));
    } else if(reason == kRecordingErrorMediaCacheReadErrorEnd) {
      DLOG(DLOG_RECORD|DLOG_ERROR, "Recording error, reason: %d(%s)\r\n", reason, mediaReportReasonString(reason));
    }

    continue_sniffing = false;
  }

  virtual void onInfo(void* user_data, MediaInfo* info) {
    DLOG(DLOG_RECORD, "Recording duration %lld.%06lld sec\r\n", info->durationUs / 1000000ll, info->durationUs % 1000000ll);
  }

  virtual void onInfoEx(void* user_data, MediaInfoEx* info) {
    int h, m, s, u;
    char normal_qsize[128], event_qsize[128], in_size[128], out_size[128], cache_in_size[128];
    char video1_size[128], video2_size[128], meta_size[128], audio_size[128], file_size[128];
    parseUsec(info->durationUs, h, m, s, u);

    DLOG0(DLOG_RECORD, "Recording duration \033[33m%02d:%02d:%02d.%06d\033[0m, wq#%d(%d), eq#%d(%d), meq#%d(%d), nohits#%u, mb.avail#%d/%d (free mem %.3fMB, cpu %.2f%%)\r\n", h, m, s, u, info->writerStat.curSamples, info->writerStat.maxSamples, info->timeshiftStat.curSamples, info->timeshiftStat.maxSamples, info->writerStat.motionStat.curSamples, info->writerStat.motionStat.maxSamples, info->mediaCacheStat.nohits_, info->mediaCacheStat.free_buffer_count_, info->mediaCacheStat.total_buffer_count_, (double)oasis::getFreeMemorySize()/1024.0/1024.0, getCPUUsage());

    //offs state
    bytesToString(info->offsStat.qNormalSize, normal_qsize);
    bytesToString(info->offsStat.qEventSize, event_qsize);

    bytesToString(info->offsStat.inSize, in_size);
    bytesToString(info->offsStat.outSize, out_size);

    bytesToString(info->mediaCacheStat.in_size_, cache_in_size);


    TRACE0("    cache: during %d msec, in %s\r\n", info->mediaCacheStat.check_duration_, cache_in_size);
    TRACE0("    offs: n#%d, e#%d, nz#%s, ez#%s, during %d msec: in %s out %s elapsed %d msec \r\n", info->offsStat.qNormalCount, info->offsStat.qEventCount, normal_qsize, event_qsize, info->offsStat.checkDuration, in_size, out_size, info->offsStat.elapsedSum);

    //TRACE0("    cache: hits#%u, nohits#%u, free#%u, gets#%u, puts#%u\r\n", info->mediaCacheStat.hits_, info->mediaCacheStat.nohits_, info->mediaCacheStat.free_buffer_count_, info->mediaCacheStat.get_buffer_count_, info->mediaCacheStat.put_buffer_count_);

    //print recording stat in details
    bytesToString(info->writerStat.video1Length, video1_size);
    bytesToString(info->writerStat.video2Length, video2_size);
    bytesToString(info->writerStat.metaLength, meta_size);
    bytesToString(info->writerStat.audioLength, audio_size);
    bytesToString(info->writerStat.fileLength, file_size);
    TRACE0("    %s: video1 %s, video2 %s, meta %s, audio %s, file %s\n", info->sniffing?"sniffing":"recording", video1_size, video2_size, meta_size, audio_size, file_size);

    if(info->writerStat.motionStat.recording) {
      bytesToString(info->writerStat.motionStat.video1Length, video1_size);
      bytesToString(info->writerStat.motionStat.video2Length, video2_size);
      bytesToString(info->writerStat.motionStat.metaLength, meta_size);
      bytesToString(info->writerStat.motionStat.audioLength, audio_size);
      bytesToString(info->writerStat.motionStat.fileLength, file_size);
      TRACE0("    motion: video1 %s, video2 %s, meta %s, audio %s, file %s\n", video1_size, video2_size, meta_size, audio_size, file_size);
    }

    if(info->timeshiftStat.recording) {
      bytesToString(info->timeshiftStat.video1Length, video1_size);
      bytesToString(info->timeshiftStat.video2Length, video2_size);
      bytesToString(info->timeshiftStat.metaLength, meta_size);
      bytesToString(info->timeshiftStat.audioLength, audio_size);
      bytesToString(info->timeshiftStat.fileLength, file_size);
      TRACE0("    event: video1 %s, video2 %s, meta %s, audio %s, file %s\n", video1_size, video2_size, meta_size, audio_size, file_size);
    }
  }

  //player paused and resumed
  virtual void onPaused(void *user_data, MediaInfo *info) {}
  virtual void onResumed(void *user_data, MediaInfo *info) {}
  virtual void onEventRecordingStarted(void* user_data, media_report_reason_t reason, const char* file_path) {}
  virtual void onEventRecordingCompleted(void* user_data, media_report_reason_t reason, const char* file_path) {}

  virtual void onMotionRecordingStarted(void *user_data, media_report_reason_t reason, const char *file_path) {
    DLOG0(DLOG_THIS, "Motion Recording started @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
  }
  virtual void onMotionRecordingCompleted(void *user_data, media_report_reason_t reason, const char *file_path) {
    DLOG0(DLOG_THIS, "Motion Recording completed @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
  }

  virtual void queryNewFilePath(void* user_data, oasis::recording_mode_t file_type, bool rear_camera_on, bool sound_on, std::string& file_path) {}
  virtual void onSnapshotCompleted(void *user_data, uint32_t snapshot_id, int32_t camera_id, int error, const std::vector<char> &image_data, const struct timeval &timestamp) {}

};


class MyMotionDetectObserver : public MotionDetectObserver 
{
 public:
  MyMotionDetectObserver() {
    created_timestamp_ = systemTime();
  }
  virtual ~MyMotionDetectObserver() {}

  virtual void onMotionDetected(int32_t camera_id, int32_t detected) {
    TRACE0("camera<%d> motion detected#%d\n", camera_id, detected);
    //after some period of stability passes
    if(systemTime() - created_timestamp_ > 5000000) {
      if(detected > 500) {
        postCommand(kStartMotionRecording);
      }
    }
  }

  uint64_t created_timestamp_;
};


void print_usage(const char *pname)
{
  DLOG0(DLOG_INFO, "USAGE: %s\n", pname);
}

void cancel_handler(int signum)
{
  timer_running = false;
  continue_sniffing = false;
}

#if 0
#define SND_PATH "default"
#else
#define SND_PATH "hw:0,0"
#endif

int main(int argc, char* argv[])
{
  int32_t err;
  int32_t i, c, n;
  std::thread t1, t2, t3;

  int32_t camera_id;
  std::list<int32_t> camera_composer_ids;

  oasis::key_value_map_t parameters;

  srand(time(NULL));

  signal(SIGINT, cancel_handler);

   ////////////////////////////////////////////////////////////////////////////////////////////
  // init

  bool offs_disabled = false;
  if(!offs_disabled) {
    parameters["offs-qsize-max"] = std::to_string(OFFS_QUEUE_SIZE_KBYTES);
    parameters["offs-overwrite-if-exist"] = "1";
    parameters["offs-cache-size"] = std::to_string(OFFS_CACHE_SIZE_KBYTES);
  } else {
    parameters["offs-disable"] = "1";
  }
  parameters["media-cache-size"] = std::to_string(MEDIA_CACHE_SIZE_KBYTES);
  //parameters["oasis-log-flags"] = std::to_string(OASIS_LOG_DEBUG/*|OASIS_LOG_ENCODE_BITRATE*/);

  //enableLogLocalTime(true);

  if(oasis::initialize(parameters) < 0) {
    DLOG(DLOG_ERROR, "Oasis init failed\n");
    return -1;
  }

  //non-offs, use system fs
#if !USE_OFFS  
  fs::offsConfigLocalFormatInfo("/tmp/DRIVING", 20*1024*1024);
  fs::offsConfigLocalFormatInfo("/tmp/EVENT", 20*1024*1024);
#endif
  ////////////////////////////////////////////////////////////////////////////////////////////
  // audio
  parameters.clear();

  parameters["types"]="source,sink";
  parameters["path"]=SND_PATH;
  parameters["always-on"]="0";
  parameters["channels"]="2";
  parameters["aec-disabled"]="1";
  parameters["snd-input-channels"]="2";
  parameters["snd-input-sample-size"]="16";
  parameters["snd-input-sampling-duration-msec"]="40";
  parameters["snd-input-sampling-rate"]="48000";
  //parameters["snd-input-sampling-rate"]="22050";

  createAudioDevice(parameters);

  ////////////////////////////////////////////////////////////////////////////////////////////
  // sources
  parameters.clear();

  parameters["source-count"] = "1";

  parameters["source1-camera-id"] = "0";
  parameters["source1-isp-id"] = "0";
  parameters["source1-isp-wdr-mode"] = "0";
  parameters["source1-capture-format"] = "YUV420";
  parameters["source1-capture-buffers"] = "5";
  parameters["source1-fps"] = "30";
  parameters["source1-subchannel-rotation"] = "0";
  parameters["source1-loc"] = "front";
  parameters["source1-capture-resolution"] = "1080p";
  parameters["source1-sensor-config"] = "./Resource_tp2863/VIC/0/tp2863_1920x1080_ch0.cfg";
  parameters["source1-autoscene-config"] = "./Resource_tp2863/AutoScene/autoscene_conf.cfg";
  parameters["source1-resource-dir"] = "./Resource_tp2863/";

  configCameras(parameters);

  ////////////////////////////////////////////////////////////////////////////////////////////
  //recorer settings
  parameters.clear();

  parameters["file-prefix"] =  "oasis-";
  parameters["file-extension"] = "mp4";
  //parameters["file-extension"] = "avi";
  parameters["file-duration-secs"] = "60";
#if USE_OFFS
  parameters["normal-folder-path"] = "/mnt/sd/DRIVING";
  parameters["event-folder-path"] = "/mnt/sd/EVENT";
  parameters["motion-folder-path"] = "/mnt/sd/EVENT";
#else
  parameters["normal-folder-path"] = "/tmp/DRIVING";
  parameters["event-folder-path"] = "/tmp/EVENT";
  parameters["motion-folder-path"] = "/tmp/EVENT";
#endif

  parameters["event-pre-recording-seconds"] = "10";
  parameters["event-post-recording-seconds"] = "10";
  parameters["motion-pre-recording-seconds"] = "10";
  parameters["motion-post-recording-seconds"] = "10";
  parameters["disable-event-recording"] = "0";
  parameters["disable-offs-recording"] = "0";
#if USE_OFFS
  parameters["max-files"] = "0";
#else  
  parameters["max-files"] = "5";
#endif  
  parameters["delete-oldest-file-on-max-files"] = "1";
  parameters["recording-size-limit-threshold-seconds"] = "1";
  parameters["report-media-info-ex"] = "1";

  parameters["avi-strd-size-max"] = "65536";
  parameters["mp4-udta-size-max"] = "65536";
  parameters["enable-persistent-cache"] = "0";
  parameters["recording-file-header-write-interval-secs"] = "1";

  parameters["snd-path"] = "hw:0,0";
  parameters["snd-input-channels"] = "2";
  parameters["snd-input-sample-size"] = "16";
  parameters["snd-input-sampling-duration-msec"] = "120";
  parameters["snd-input-sampling-rate"] = "44100";
  //parameters["snd-input-sampling-rate"] = "48000";

  parameters["aencoder-type"] = "aac"; //aac, raw, mp3
  parameters["aencoder-bitrate"] = "128000";

  parameters["osd-font-size"] = "0"; // disable (default)
  parameters["osd-font-face"] = "Consolas";
  parameters["osd-text-color"] = "255,255,255";
  parameters["osd-use-text-color"] = "1";
  parameters["osd-use-bg-color"] = "0";
  parameters["osd-bg-color"] = "0,0,0";
  parameters["osd-use-outline-color"] = "1";
  parameters["osd-outline-color"] = "255,255,255";
  parameters["osd-horz-align"] = "left";
  parameters["osd-vert-align"] = "bottom";
  parameters["osd-font-path"] = "/mnt/flash/leipzig/consola.ttf";

  parameters["channel-count"] = "1";

  parameters["channel1-camera-id"] = "0";
  parameters["channel1-ise-id"] = "-1";
  parameters["channel1-resolution"] = "1080p";
  parameters["channel1-bitrate"] = "8000000";
  parameters["channel1-fps"] = "30";
  parameters["channel1-file-framerate"] = "30";
  parameters["channel1-vencoder-type"] = "h264";
  parameters["channel1-venc-framerate"] = "30";
  parameters["channel1-venc-keyframe-interval"] = "30";
  parameters["channel1-h264-profile"] = "high";
  parameters["channel1-h264-level"] = "level51";
  parameters["channel1-h264-min-qp"] = "10";
  parameters["channel1-h264-max-qp"] = "31";
  parameters["channel1-h264-enable-fixqp"] = "0";
  parameters["channel1-h264-fix-iqp"] = "10";
  parameters["channel1-h264-fix-pqp"] = "20";

  parameters["channel1-media-wait-timeout-secs"] = "3";
  parameters["channel1-media-wait-timeout-notify-oneshot"] = "1";
  parameters["channel1-osd-font-size"] = "0"; //disable;

  std::shared_ptr<MyRecordingObserver> recording_observer = std::make_shared<MyRecordingObserver>();

  RecorderRef recorder = createRecorder(parameters);
  if(recorder == nullptr) {
    DLOG(DLOG_ERROR, "recorder creation failed!\r\n");
    oasis::finalize();
    return -1;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  //motion detector settings

  std::shared_ptr<MyMotionDetectObserver> motion_detect_observer = std::make_shared<MyMotionDetectObserver>();  

  parameters.clear();

  parameters["camera-id"] = "0";
  parameters["enable"] = "1";
  parameters["use-g2d"] = "1";
  parameters["buffer-count"] = "3";
  parameters["sensitivity-level"] = "5";
  parameters["fps"] = "6";
  parameters["version"] = "2";
  parameters["height-max"] = "135";

  MotionDetectorRef motion_detector = createMotionDetector(parameters);
  if(motion_detector == nullptr) {
    DLOG(DLOG_ERROR, "motion detector creation failed!\r\n");
    destroyRecorder(recorder);
    oasis::finalize();
    return -1;
  }


  memset(&strd_data_header_, 0, sizeof(strd_data_header_));


  t3 = std::thread([&]() {

    RecorderCommand cmd;

    do {
      {
        std::unique_lock<std::mutex> _l(cmdq_lock_);
        while(cmdq_.empty()) {
          cmdq_cond_.wait(_l);
        }
        cmd = cmdq_.front();
        cmdq_.pop_front();
      }

      if(cmd.cmd_ == kRecorderExitNow) {
        DLOG(DLOG_TRACE, "got kRecorderExitNow\r\n");
        break;
      }

      DLOG(DLOG_INFO, "cmd %d\n", cmd.cmd_);

      switch(cmd.cmd_) {
        case kStartMotionRecording: {
          if (recorderIsRunning(recorder) && isMotionRecording(recorder) == false) {
            time_t t;
            struct tm tm_t;
            char path[PATH_MAX];

            time(&t);
            localtime_r(&t, &tm_t);

    #if USE_OFFS
            sprintf(path, "/mnt/sd/EVENT/MTN_%04d-%02d-%02d_%02d-%02d-%02d.mp4", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
    #else
            sprintf(path, "/tmp/EVENT/MTN_%04d-%02d-%02d_%02d-%02d-%02d.mp4", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
    #endif
            startMotionRecording(recorder, path, nullptr, 0);
          } else {
            fprintf(stdout, "Recorder not started (%d) or busy in recording an motion (%d).\n", recorderIsRunning(recorder), isMotionRecording(recorder));
          }
          break;
        }
      default:
          break;
      }

    } while(1);

    {
      std::lock_guard<std::mutex> _l(cmdq_lock_);
      cmdq_.clear();
    }
  });


  ////////////////////////////////////////////////////////////////////////////////////////////
  t2 = std::thread([&]() {
    time_t t;
    struct tm tm_t;
    uint64_t usec, preview_usec;
    std::string title;
    uint32_t count = 0;

    char stream_data[512] = { 0, };

    preview_usec = systemTime();

    do {

      usec = systemTime();

      time(&t);
      localtime_r(&t, &tm_t);

      if (isRecorderRecording(recorder)) {
        sprintf(stream_data, "Main %4d/%02d/%02d %02d:%02d:%02d", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
        addRecordingVideoStreamData(recorder, (void*)stream_data, strlen(stream_data) + 1);
      }

      if(/*count == 0 &&*/ isRecorderRecording(recorder)) {
        title = oasis::format("OASIS %4d/%02d/%02d %02d:%02d:%02d\n", tm_t.tm_year + 1900, tm_t.tm_mon + 1, tm_t.tm_mday, tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
        //title += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero.";
        setRecordingText(recorder, kTextTrackOsd, title, usec);
        count++;
      }

      //30 msec
      usleep(40000);

    } while (timer_running);
  });

  //RUN as SNIFFING mode!!!
  err = startRecording(recorder, recording_observer, nullptr, true, &strd_data_header_, sizeof(strd_data_header_));
  if(err < 0) {
    DLOG(DLOG_ERROR, "start recording failed\r\n");
    goto done;
  }  

  err = motionDetectorStart(motion_detector, motion_detect_observer);
  if(err < 0) {
    DLOG0(DLOG_ERROR, "MotionDetectgor start failed\n");
    goto done;
  }

  TRACE0("MotionDetector<%d> started\n", motionDetectorId(motion_detector));

  do {
    c = getch("Ctrl+C to exit...");
    if(c < 0 || continue_sniffing == false) {
      break;
    }
    if(c != '\n') {
      fprintf(stdout, "%c", c);
    }
    fprintf(stdout, "\n");
  } while (continue_sniffing == true);

done:
  timer_running = false;
  if (t2.joinable()) {
    t2.join();
  }

  postCommand(kRecorderExitNow);
  if (t3.joinable()) {
    t3.join();
  }

  if(recorder) {
    destroyRecorder(recorder);
  }

  if(motion_detector) {
    destroyMotionDetector(motion_detector);
  }  

  oasis::finalize();

  printf("goodbye.\n");

  return 0;
}