🌐 한국어

    이벤트 녹화#

    녹화 중에 이벤트가 발생되면 녹화 객체 생성시 전달한 패러미터(event-pre-recording-secondsevent-post-recording-seconds) 값으로 지정된 전후 시간만큼 이벤트 녹화를 시작합니다.

    이벤트 녹화는 아래 API를 사용합니다.

    구분 녹화 시작 녹화 종료
    통합 녹화 startEventRecording stopEventRecording
    개별 녹화 startMultiChannelEventRecording stopMultiChannelEventRecording

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

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

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

    통합 녹화#

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

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

    • int32_t startEventRecording(RecorderRef recorder, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size);

    • int32_t startEventRecording(RecorderRef recorder, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size, bool do_single_recording);
      • do_single_recordingtrue이면 현재 녹화를 중지하고 이벤트 녹화를 시작합니다. 이벤트 녹화가 끝나면 다시 현재 녹화를 시작합니다.

    개별 녹화#

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

    • int32_t startMultiChannelEventRecording(MultiChannelRecorderRef multi_channel_recorder, const std::list<MultiChannelRecorderFilePath> & file_paths, void * stream_data_header, size_t stream_data_header_size, bool do_single_recording);
      • file_paths 에 각 채널별 녹화파일 경로를 추가하여 채널 개수 크기의 목록을 만들어 전달합니다.

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

    • int32_t startMultiChannelEventRecording(MultiChannelRecorderRef multi_channel_recorder, int32_t channel_id, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size, bool is_type_of_motion);
      • is_type_of_motiontrue이면 모션 녹화를 진행합니다. 이벤트 녹화와 차이점은 녹화 생성시 지정된 전후 시간을 motion-pre-recording-secondsmotion-post-recording-seconds 값을 따릅니다.

    • int32_t startMultiChannelEventRecording(MultiChannelRecorderRef multi_channel_recorder, int32_t channel_id, const char * event_recording_file_path, void * stream_data_header, size_t stream_data_header_size, bool is_type_of_motion, bool do_single_recording);

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

    예제#

    통합 이벤트 녹화#

    아래는 2개의 영상 채널에 대한 통합 녹화 예입니다. 녹화 도중 사용자가 엔터키를 입력하면 이벤트 녹화를 진행합니다.

      printf("Ctrl+C to exit...\n");
    
      do {
        c = getch("Press Enter key to start event recording...");
        if(c < 0 || continue_recording == false) {
          break;
        }
        if(c != '\n') {
          fprintf(stdout, "%c", c);
        }
        fprintf(stdout, "\n");
    
        if(c == 10) {
          //start event recording
          if (isRecorderRecording(recorder) && isEventRunning(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/EVT_%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/EVT_%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
            startEventRecording(recorder, path);
          } else {
            fprintf(stdout, "Recorder not started or busy in recording an event.\n");
          }
        }
      } while (continue_recording == true);
    

    로그 출력을 위해 MediaObserver 에서 유도된 사용자 정의 인터페이스에 아래와 같이 콜백 함수를 정의합니다.

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

    출력된 로그 내용입니다.

    [2000/06/27 04:02:35.233130] Event Recording started @ "/tmp/EVENT/EVT_2000-06-27_04-02-35.mp4" (reason: 0 NoError).
    [2000/06/27 04:02:42.338952] Event Recording completed @ "/tmp/EVENT/EVT_2000-06-27_04-02-35.mp4" (reason: 0 NoError).
    

    전체 코드입니다.

    #include "OasisAPI.h"
    #include "OasisLog.h"
    #include "OasisMedia.h"
    #include "OasisFS.h"
    #include "OasisUtil.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 bool timer_running = true;
    static bool continue_recording = 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);
      }
    }
    
    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_recording = 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_recording = 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) {
        dumpInfoEx(info);
      }
    
      //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) {
        DLOG0(DLOG_THIS, "Event Recording started @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
      }
      virtual void onEventRecordingCompleted(void* user_data, media_report_reason_t reason, const char* file_path) {
        DLOG0(DLOG_THIS, "Event Recording completed @ \"%s\" (reason: %d %s).\r\n", file_path, reason, mediaReportReasonString(reason));
      }
    
      virtual void onMotionRecordingStarted(void *user_data, media_report_reason_t reason, const char *file_path) {
    
      }
      virtual void onMotionRecordingCompleted(void *user_data, media_report_reason_t reason, const char *file_path) {
    
      }
    
      virtual void queryNewFilePath(void* user_data, oasis::recording_mode_t file_type, bool rear_camera_on, bool sound_on, std::string& file_path) {
        // use as it is
      }
      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) {}
    
    };
    
    
    void print_usage(const char *pname)
    {
      DLOG0(DLOG_INFO, "USAGE: %s\n", pname);
    }
    
    void cancel_handler(int signum)
    {
      timer_running = false;
      continue_recording = false;
    }
    
    #if 0
    #define SND_PATH "default"
    #else
    #define SND_PATH "hw:0,0"
    #endif
    
    int main(int argc, char* argv[])
    {
      int32_t c, err;
      std::thread t2;
    
      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["denoise-enabled"]="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"] = "2";
    
      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"] = "2160p";
      parameters["source1-sensor-config"] = "./Resource_678/VIC/2/imx678_3840x2160_ch2.cfg";
      parameters["source1-autoscene-config"] = "./Resource_678/AutoScene/autoscene_conf.cfg";
      parameters["source1-resource-dir"] = "./Resource_678/";
    
      parameters["source2-camera-id"] = "1";
      parameters["source2-isp-id"] = "0";
      parameters["source2-isp-wdr-mode"] = "0";
      parameters["source2-capture-format"] = "YUV420";
      parameters["source2-capture-buffers"] = "5";
      parameters["source2-fps"] = "30";
      parameters["source2-subchannel-rotation"] = "0";
      parameters["source2-loc"] = "right";
      parameters["source2-capture-resolution"] = "1080p";
      parameters["source2-sensor-config"] = "./Resource_tp2863/VIC/0/tp2863_1920x1080_ch0.cfg";
      parameters["source2-autoscene-config"] = "./Resource_tp2863/AutoScene/autoscene_conf.cfg";
      parameters["source2-resource-dir"] = "./Resource_tp2863/";
    
      configCameras(parameters);
    
      ////////////////////////////////////////////////////////////////////////////////////////////
      //recorer settings
      parameters.clear();
    
      parameters["file-prefix"] =  "oasis-";
      parameters["file-extension"] = "mp4";
      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"] = "30";
      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["osd-use-fixed-size"] = "0";
    
      parameters["channel-count"] = "2";
    
      parameters["channel1-camera-id"] = "0";
      parameters["channel1-ise-id"] = "-1";
      parameters["channel1-resolution"] = "2160p";
      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-enable-cabac"] = "1";
      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"] = "12";
    
      parameters["channel2-camera-id"] = "1";
      parameters["channel2-ise-id"] = "-1";
      parameters["channel2-resolution"] = "1080p";
      parameters["channel2-bitrate"] = "8000000";
      parameters["channel2-fps"] = "30";
      parameters["channel2-file-framerate"] = "30";
      parameters["channel2-vencoder-type"] = "h264";
      parameters["channel2-venc-framerate"] = "30";
      parameters["channel2-venc-keyframe-interval"] = "30";
      parameters["channel2-h264-profile"] = "high";
      parameters["channel2-h264-level"] = "level51";
      parameters["channel2-h264-enable-cabac"] = "1";
      parameters["channel2-h264-min-qp"] = "10";
      parameters["channel2-h264-max-qp"] = "31";
      parameters["channel2-h264-enable-fixqp"] = "0";
      parameters["channel2-h264-fix-iqp"] = "10";
      parameters["channel2-h264-fix-pqp"] = "20";
      parameters["channel2-media-wait-timeout-secs"] = "3";
      parameters["channel2-media-wait-timeout-notify-oneshot"] = "1";
      parameters["channel2-osd-font-size"] = "12";
    
    
      std::shared_ptr<MyRecordingObserver> recording_observer = std::make_shared<MyRecordingObserver>();
      //dumpParameters("recorder", recorder_parameters);
    
      RecorderRef recorder = createRecorder(parameters);
      if(recorder == nullptr) {
        DLOG(DLOG_ERROR, "recorder creation failed!\r\n");
        goto done;
      }
    
      memset(&strd_data_header_, 0, sizeof(strd_data_header_));
    
      ////////////////////////////////////////////////////////////////////////////////////////////
      // timer thread
      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);
            setRecordingText(recorder, kTextTrackOsd, title, usec);
            count++;
          }
    
          //30 msec
          usleep(40000);
    
        } while (timer_running);
      });
    
      err = startRecording(recorder, recording_observer, nullptr, false, &strd_data_header_, sizeof(strd_data_header_));
      if(err < 0) {
        DLOG(DLOG_ERROR, "start recording failed\r\n");
        goto done;
      }
    
      printf("Ctrl+C to exit...\n");
    
      do {
        c = getch("Press Enter key to start event recording...");
        if(c < 0 || continue_recording == false) {
          break;
        }
        if(c != '\n') {
          fprintf(stdout, "%c", c);
        }
        fprintf(stdout, "\n");
    
        if(c == 10) {
          //start event recording
          if (isRecorderRecording(recorder) && isEventRunning(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/EVT_%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/EVT_%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
            startEventRecording(recorder, path);
          } else {
            fprintf(stdout, "Recorder not started or busy in recording an event.\n");
          }
        }
    
    
      } while (continue_recording == true);
    
    done:
    
      timer_running = false;
      if (t2.joinable()) {
        t2.join();
      }
    
      if(recorder) {
        destroyRecorder(recorder);
      }
    
    
      oasis::finalize();
    
      printf("goodbye.\n");
    
      return 0;
    }
    
    

    개별 이벤트 녹화#

    아래는 2개의 영상 채널에 대한 개별 녹화 예입니다. 녹화 도중 사용자가 엔터키를 입력하면 이벤트 녹화를 진행합니다.

      printf("Ctrl+C to exit...\n");
    
      do {
        c = getch("Press Enter key to start event recording...");
        if(c < 0 || continue_recording == false) {
          break;
        }
        if(c != '\n') {
          fprintf(stdout, "%c", c);
        }
        fprintf(stdout, "\n");
    
        if(c == 10) {
          //start event recording
          if (isMultiChannelRecorderRecording(recorder) && isMultiChannelEventRunning(recorder) == false) {
            std::list<MultiChannelRecorderFilePath> file_paths;
    
            time_t t;
            struct tm tm_t;
            char path[PATH_MAX];
    
            time(&t);
            localtime_r(&t, &tm_t);
    
            for(int32_t ch=1; ch<=2; ch++) {
    #if USE_OFFS
              sprintf(path, "/mnt/sd/EVENT/EVT_%04d-%02d-%02d_%02d-%02d-%02d-CH%d.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, ch);
    #else
              sprintf(path, "/tmp/EVENT/EVT_%04d-%02d-%02d_%02d-%02d-%02d-CH%d.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, ch);
    #endif
              file_paths.emplace_back(ch, path);
            }
    
            startMultiChannelEventRecording(recorder, file_paths, nullptr, 0, false);
          } else {
            fprintf(stdout, "Recorder not started or busy in recording an event.\n");
          }
        }
      } while (continue_recording == true);
    
    

    로그 출력을 위해 MediaObserver2 에서 유도된 사용자 정의 인터페이스에 아래와 같이 콜백 함수를 정의합니다.

    virtual void onEventRecordingStarted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
      DLOG0(DLOG_THIS, "Event<%d> Recording started @ \"%s\" (reason: %d).\r\n", channel_id, file_path, reason);
    }
    
    virtual void onEventRecordingCompleted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
      DLOG0(DLOG_THIS, "Event<%d> Recording completed @ \"%s\" (reason: %d).\r\n", channel_id, file_path, reason);
    }
    

    출력된 로그 내용입니다.

    [264.401025] [2000/06/27 04:06:13.770276] Event<1> Recording started @ "/tmp/EVENT/EVT_2000-06-27_04-06-13-CH1.mp4" (reason: 0).
    [264.401372] [2000/06/27 04:06:13.770622] Event<2> Recording started @ "/tmp/EVENT/EVT_2000-06-27_04-06-13-CH2.mp4" (reason: 0).
    
    
    [279.613067] [2000/06/27 04:06:28.982317] Event<2> Recording completed @ "/tmp/EVENT/EVT_2000-06-27_04-06-13-CH2.mp4" (reason: 0).
    [279.616044] [2000/06/27 04:06:28.985295] Event<1> Recording completed @ "/tmp/EVENT/EVT_2000-06-27_04-06-13-CH1.mp4" (reason: 0).
    

    전체 코드입니다.

    
    #include "OasisAPI.h"
    #include "OasisLog.h"
    #include "OasisMedia.h"
    #include "OasisFS.h"
    #include "OasisUtil.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 bool timer_running = true;
    static bool continue_recording = 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);
      }
    }
    
    static uint8_t strd_data_header_[24] = {0, };
    
    
    class MyRecordingObserver : public oasis::MediaObserver2 
    {
    public:
      MyRecordingObserver() {}
      virtual ~MyRecordingObserver() {}
    
      //recording started and stopped
      virtual void onStarted(int32_t channel_id, void *user_data, const char *file_path) {
        DLOG(DLOG_THIS, "Recording<%d> started, file path \"%s\"\r\n", channel_id, file_path);
      }
      virtual void onStopped(int32_t channel_id, void *user_data, media_report_reason_t reason, void *details) {
        DLOG(DLOG_THIS, "Recording%d> stopped, reason: %d(%s)\r\n", channel_id, reason, mediaReportReasonString(reason));
        if(reason != kMediaReportNoError) {
          continue_recording = false;
        }    
      }
      //recording a new file
      virtual void onFileChanged(int32_t channel_id, void *user_data, const char *new_file_path) {
        DLOG(DLOG_THIS, "Recording<%d> new file used: \"%s\"\r\n", channel_id, new_file_path);
      }
      virtual void onError(int32_t channel_id, void *user_data, media_report_reason_t reason, void *details) {
        if(reason == kRecordingErrorMediaDataTimeout) {
          int32_t camera_id = (intptr_t)details;
          DLOG(DLOG_ERROR, "Recording<%d> error, reason: %d(%s), camera<%d>\r\n", channel_id, reason, mediaReportReasonString(reason), camera_id);
        } else if(reason == kRecordingErrorMediaCacheReadErrorBegin) {
          DLOG(DLOG_ERROR, "Recording<%d> error, reason: %d(%s) ===> stopping!!!\r\n", channel_id, reason, mediaReportReasonString(reason));
        } else if(reason == kRecordingErrorMediaCacheReadErrorEnd) {
          DLOG(DLOG_ERROR, "Recording<%d> error, reason: %d(%s)\r\n", channel_id, reason, mediaReportReasonString(reason));
        }    
        continue_recording = false;    
      }
    
      //periodic report every 1 second.
      virtual void onInfo(int32_t channel_id, void *user_data, MediaInfo *info) {
        DLOG(DLOG_THIS, "Recording<%d> duration %lld.%06lld sec\r\n", channel_id, info->durationUs / 1000000ll, info->durationUs % 1000000ll);    
      }
      virtual void onInfoEx(int32_t channel_id, void *user_data, MediaInfoEx *info) {
    #if 0   
        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);
    
        TRACE0("Recording<%d> 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", channel_id, 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);
        }
    #endif
      }
    
      //event recording started and completed(aborted)
      virtual void onEventRecordingStarted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
        DLOG0(DLOG_THIS, "Event<%d> Recording started @ \"%s\" (reason: %d).\r\n", channel_id, file_path, reason);
      }
      virtual void onEventRecordingCompleted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
        DLOG0(DLOG_THIS, "Event<%d> Recording completed @ \"%s\" (reason: %d).\r\n", channel_id, file_path, reason);
      }
    
      //motion recording started and completed(aborted)
      virtual void onMotionRecordingStarted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
    
      }
      virtual void onMotionRecordingCompleted(int32_t channel_id, void *user_data, media_report_reason_t reason, const char *file_path) {
    
      }
    
      virtual void queryNewFilePaths(void *user_data, recording_mode_t file_type, bool sound_on, std::list<MultiChannelRecorderFilePath> &file_paths) {
        DLOG(DLOG_THIS, "queryNewFilePath: mode: %d, sound on %d:\n", file_type, sound_on);
        for(auto it = file_paths.begin(); it != file_paths.end(); it++) {
          DLOG(DLOG_THIS, "     Recording<%d> path %s\n", (*it).channel_id_, (*it).file_path_.c_str());
        }
      }
    
      virtual void onSnapshotCompleted(int32_t channel_id, void *user_data, uint32_t snapshot_id, int error, const std::vector<char> &jpeg_image_data, const struct timeval &timestamp) {
        DLOG(DLOG_THIS, "Snapshot<%d> completed: id %d, error %d, size %zd bytes @ %d sec\n", channel_id, snapshot_id, error, jpeg_image_data.size(), timestamp.tv_sec);
      }
    
    };
    
    
    void print_usage(const char *pname)
    {
      DLOG0(DLOG_INFO, "USAGE: %s\n", pname);
    }
    
    void cancel_handler(int signum)
    {
      timer_running = false;
      continue_recording = false;
    }
    
    #if 0
    #define SND_PATH "default"
    #else
    #define SND_PATH "hw:0,0"
    #endif
    
    int main(int argc, char* argv[])
    {
      int32_t c, err;
      std::thread t2;
    
      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["denoise-enabled"]="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"] = "2";
    
      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"] = "2160p";
      parameters["source1-sensor-config"] = "./Resource_678/VIC/2/imx678_3840x2160_ch2.cfg";
      parameters["source1-autoscene-config"] = "./Resource_678/AutoScene/autoscene_conf.cfg";
      parameters["source1-resource-dir"] = "./Resource_678/";
    
      parameters["source2-camera-id"] = "1";
      parameters["source2-isp-id"] = "0";
      parameters["source2-isp-wdr-mode"] = "0";
      parameters["source2-capture-format"] = "YUV420";
      parameters["source2-capture-buffers"] = "5";
      parameters["source2-fps"] = "30";
      parameters["source2-subchannel-rotation"] = "0";
      parameters["source2-loc"] = "right";
      parameters["source2-capture-resolution"] = "1080p";
      parameters["source2-sensor-config"] = "./Resource_tp2863/VIC/0/tp2863_1920x1080_ch0.cfg";
      parameters["source2-autoscene-config"] = "./Resource_tp2863/AutoScene/autoscene_conf.cfg";
      parameters["source2-resource-dir"] = "./Resource_tp2863/";
    
    
      configCameras(parameters);
    
      ////////////////////////////////////////////////////////////////////////////////////////////
      //recorer settings
      parameters.clear();
    
      parameters["file-prefix"] =  "oasis-";
      parameters["file-extension"] = "mp4";
      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"] = "30";
      parameters["disable-event-recording"] = "0";
      parameters["disable-offs-recording"] = "0";
    #if USE_OFFS
      parameters["max-files"] = "0";
    #else  
      parameters["max-files"] = "15";
    #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["osd-use-fixed-size"] = "0";
    
      parameters["channel-count"] = "2";
    
      parameters["channel1-camera-id"] = "0";
      parameters["channel1-ise-id"] = "-1";
      parameters["channel1-resolution"] = "2160p";
      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-enable-cabac"] = "1";
      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"] = "12";
    
      parameters["channel2-camera-id"] = "1";
      parameters["channel2-ise-id"] = "-1";
      parameters["channel2-resolution"] = "1080p";
      parameters["channel2-bitrate"] = "8000000";
      parameters["channel2-fps"] = "30";
      parameters["channel2-file-framerate"] = "30";
      parameters["channel2-vencoder-type"] = "h264";
      parameters["channel2-venc-framerate"] = "30";
      parameters["channel2-venc-keyframe-interval"] = "30";
      parameters["channel2-h264-profile"] = "high";
      parameters["channel2-h264-level"] = "level51";
      parameters["channel2-h264-enable-cabac"] = "1";
      parameters["channel2-h264-min-qp"] = "10";
      parameters["channel2-h264-max-qp"] = "31";
      parameters["channel2-h264-enable-fixqp"] = "0";
      parameters["channel2-h264-fix-iqp"] = "10";
      parameters["channel2-h264-fix-pqp"] = "20";
      parameters["channel2-media-wait-timeout-secs"] = "3";
      parameters["channel2-media-wait-timeout-notify-oneshot"] = "1";
      parameters["channel2-osd-font-size"] = "12";
    
    
    
      std::shared_ptr<MyRecordingObserver> recording_observer = std::make_shared<MyRecordingObserver>();
      //dumpParameters("recorder", recorder_parameters);
    
      RecorderRef recorder = createMultiChannelRecorder(parameters);
      if(recorder == nullptr) {
        DLOG(DLOG_ERROR, "recorder creation failed!\r\n");
        goto done;
      }
    
      memset(&strd_data_header_, 0, sizeof(strd_data_header_));
    
      ////////////////////////////////////////////////////////////////////////////////////////////
      // timer thread
      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 (isMultiChannelRecorderRecording(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);
            addMultiChannelRecordingVideoStreamData(recorder, (void*)stream_data, strlen(stream_data) + 1);
          }
    
          if(/*count == 0 &&*/ isMultiChannelRecorderRecording(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);
            setMultiChannelRecordingText(recorder, kTextTrackOsd, title, usec);
            count++;
          }
    
          //30 msec
          usleep(40000);
    
        } while (timer_running);
      });
    
      err = startMultiChannelRecording(recorder, recording_observer, nullptr, false, &strd_data_header_, sizeof(strd_data_header_));
      if(err < 0) {
        DLOG(DLOG_ERROR, "start recording failed\r\n");
        goto done;
      }
    
    
      printf("Ctrl+C to exit...\n");
    
      do {
        c = getch("Press Enter key to start event recording...");
        if(c < 0 || continue_recording == false) {
          break;
        }
        if(c != '\n') {
          fprintf(stdout, "%c", c);
        }
        fprintf(stdout, "\n");
    
        if(c == 10) {
          //start event recording
          if (isMultiChannelRecorderRecording(recorder) && isMultiChannelEventRunning(recorder) == false) {
            std::list<MultiChannelRecorderFilePath> file_paths;
    
            time_t t;
            struct tm tm_t;
            char path[PATH_MAX];
    
            time(&t);
            localtime_r(&t, &tm_t);
    
            for(int32_t ch=1; ch<=2; ch++) {
    #if USE_OFFS
              sprintf(path, "/mnt/sd/EVENT/EVT_%04d-%02d-%02d_%02d-%02d-%02d-CH%d.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, ch);
    #else
              sprintf(path, "/tmp/EVENT/EVT_%04d-%02d-%02d_%02d-%02d-%02d-CH%d.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, ch);
    #endif
              file_paths.emplace_back(ch, path);
            }
    
            startMultiChannelEventRecording(recorder, file_paths, nullptr, 0, false);
          } else {
            fprintf(stdout, "Recorder not started or busy in recording an event.\n");
          }
        }
    
    
      } while (continue_recording == true);
    
    done:
    
      timer_running = false;
      if (t2.joinable()) {
        t2.join();
      }
    
      if(recorder) {
        destroyMultiChannelRecorder(recorder);
      }
    
    
      oasis::finalize();
    
      printf("goodbye.\n");
    
      return 0;
    }