Timelapse Recording#
Timelapse recording is a technique where the video is recorded over a long period by keeping the encoding frame rate lower than the actual video frame rate, while the playback frame rate is higher than the encoding frame rate. This creates a fast-forward effect during playback.
For instance, if you record at only 1 frame per second from a camera that captures video at a 30 frame rate, and play it back at a 30 frame rate, the actual 30 seconds of recorded video will be played back in 1 second. During playback, 1 frame represents 1 actual second.
Note
Audio is not saved during timelapse recording.
Note
Event recording triggered during timelapse mode proceeds as normal recording, saving both audio and video using the file-framerate.
Proceed in the following sequence:
- Stop the recording if individual or combined recording is active.
- Apply changes to the following recording parameter key values on the created recording object, or apply them when creating a recording object:
- normal-folder-path
- event-folder-path
- file-duration-secs
- channel<N>-file-framerate
- channel<N>-venc-framerate
- Start the individual or combined recording.
Restart the recording after stopping it.
The calculation for the file-duration-secs value is shown in the formula below:
Example of Switching from Normal Recording to Timelapse Recording#
Below is an example of switching from normal recording to timelapse recording during execution on the channel1 camera.
First, stop the recording.
if(recorderIsRunning(recorder_)) {
err = stopRecording(recorder_);
}
If necessary, change the locations of the normal recording folder and event recording folder to the timelapse folders.
parameters["normal-folder-path"] = timelapse_folder_;
parameters["event-folder-path"] = timelapse_event_folder_;
Change the recording file playback frame rate and the video encoding frame rate to 30 and 1, respectively.
parameters["channel1-file-framerate"] = std::to_string(30);
parameters["channel1-venc-framerate"] = std::to_string(1);
Change the recording duration of the recording file to the timelapse base time.
If the total playback duration of the recording file is 60 seconds, the recording file playback frame rate is 30fps, and the encoding frame rate is 1fps, the total timelapse duration is \(60 \times (30 / 1) = 1800\) seconds. If the encoding frame rate is 5fps, it is \(60 \times (30 / 5) = 360\) seconds.
parameters["file-duration-secs"] = std::to_string(1800);
Modify the configuration settings of the recorder object.
changeRecorderParameters(recorder_, parameters);
Restart the recording.
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);
Example of Event Recording During Timelapse Recording#
The example below demonstrates timelapse recording for a single video channel. When the user presses the Enter key, event recording is executed. Event recording operates as normal recording and includes both video and audio.
#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) {
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) {
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);
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";
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";
//Set as the total timelapse recording duration
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["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";
//Video input framerate
parameters["channel1-fps"] = "30";
//File playback framerate
parameters["channel1-file-framerate"] = "30";
//Encoding framerate
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);
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;
}