타입랩스(Timelapse) 녹화¶
타임랩스 녹화는 인코딩 프레임 레이트를 실제 영상 프레임 레이트 보다 낮게 하면서, 재생 프레임 레이트는 인코딩 프레임 레이트보다 높게하여 장시간동안 영상을 녹화하는 방식으로 재생시에는 빠르게 보기 같은 효과가 있습니다.
30 프레임 레이트로 영상을 촬여하는 카메라로 부터 초당 1 프레임만 녹화하고, 재생을 30 프레임 레이트로 하게 되면, 재생할 때 실제 30초 동안의 영상을 1초 동안 재생하게 됩니다. 재생할 때 1 프레임은 실제 1초가 됩니다.
Note
타임랩스 녹화에는 음성이 저장되지 않습니다.
Note
타임랩스 모드 녹화 중 이벤트 녹화는 일반 녹화로 진행되어 file-framerate로 음성 영상이 모두 저장됩니다.
아래 순서대로 진행합니다.
- 개별 또는 통합 녹화가 시작된 상태면 녹화를 중지합니다.
- 아래 녹화 패러미터 키 값을 생성한 녹화 객체에 변경 또는 녹화 객체 생성 시 적용합니다.
- normal-folder-path
- event-folder-path
- file-duration-secs
- channel<N>-file-framerate
- channel<N>-venc-framerate
- 개별 또는 통합 녹화를 시작합니다.
녹화를 정지하고 한 후 녹화를 재시작합니다.
file-duration-secs 값 계산은 아래 수식과 같습니다.
일반 녹화에서 타임랩스 녹화로 변경 예¶
아래는 channel<1> 카메라에서 일반 녹화 도중 타임랩스 녹화로 변경하는 예입니다.
우선 녹화를 중지합니다.
if(recorderIsRunning(recorder_)) {
err = stopRecording(recorder_);
}
필요할 경우, 일반 녹화 폴더와 이벤트 녹화 폴더 위치를 타임랩스 폴더로 변경합니다.
parameters["normal-folder-path"] = timelapse_folder_;
parameters["event-folder-path"] = timelapse_event_folder_;
녹화 파일 재생 프레임 레이트와 영상 인코딩 프레임 레이트를 각각 30, 1 로 변경합니다.
parameters["channel1-file-framerate"] = std::to_string(30);
parameters["channel1-venc-framerate"] = std::to_string(1);
녹화 파일의 녹화 시간을 타임랩스 기준 시간으로 변경합니다.
녹화 파일 총 재생 시간이 60초이고, 녹화 파일 재생 프레임 레이트가 30fps 이고, 인코딩 프레임 레이트가 1fps이면, 타임랩스의 총 시간은 60x(30/1) = 1800 초입니다. 만일 인코딩 프레임 레이트가 5fps이면 60x(30/5) = 360 초입니다.
parameters["file-duration-secs"] = std::to_string(1800);
녹화 객체의 환경 설정을 변경합니다.
changeRecorderParameters(recorder_, parameters);
녹화를 재시작합니다.
err = startRecording(recorder_, observer_, nullptr);
oasis::key_value_map_t parameters;
if(recorderIsRunning(recorder_)) {
stopRecording(recorder_);
}
// file-duration-secs = 60
// timelapse-file-framerate=30
// timelapse-venc-framerate=1
// ; play duration formula:
// ; total frames = file-duration-secs*timelapse-file-framerate = 60 * 30 = 1800 frames
// ; recording duration = total-frames/timelapse-venc-framerate = 1800 / 1 = 1800 seconds
// timelapse-file-duration-secs=1800
parameters["normal-folder-path"] = timelapse_folder_;
parameters["event-folder-path"] = timelapse_event_folder_;
parameters["file-duration-secs"] = std::to_string(timelapse_record_duration_);
for(int32_t camera_id=1; camera_id<=channel_count_; camera_id++) {
sprintf(key, "channel%d-file-framerate", camera_id);
parameters[key] = std::to_string(timelapse_file_framerate_);
sprintf(key, "channel%d-venc-framerate", camera_id);
parameters[key] = std::to_string(timelapse_venc_framerate_);
}
changeRecorderParameters(recorder_, parameters);
startRecording(recorder_, observer_, nullptr);
타임랩스 녹화 중 이벤트 녹화 예¶
아래 예는 한개의 영상채널을 타임랩스 녹화하는 예입니다. 사용자가 엔터키를 치면 이벤트 녹화를 진행합니다. 이벤트 녹화에는 일반 녹화로 진행되며 영상과 음성 모두 포함됩니다.
#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 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);
}
}
enum {
kStartEventRecording = 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_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) {
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) {
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) {
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 ×tamp) {}
};
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 err;
int32_t i, c, n;
std::thread t1, t2, t3;
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-duration-secs"] = "1800";
#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-venc-framerate"] = "1";
parameters["channel1-vencoder-type"] = "h264";
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;
}
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 kStartEventRecording: {
if (recorderIsRunning(recorder) && isEventRecording(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, 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);
});
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) {
postCommand(kStartEventRecording);
}
} while (continue_recording == true);
done:
timer_running = false;
if (t2.joinable()) {
t2.join();
}
postCommand(kRecorderExitNow);
if (t3.joinable()) {
t3.join();
}
if(recorder) {
destroyRecorder(recorder);
}
oasis::finalize();
printf("goodbye.\n");
return 0;
}