diff --git a/wrappers/nodejs/index.js b/wrappers/nodejs/index.js index 954cce9a27..4a414360be 100644 --- a/wrappers/nodejs/index.js +++ b/wrappers/nodejs/index.js @@ -17,7 +17,6 @@ const fs = require('fs'); class Device { constructor(dev) { this.cxxDev = dev; - this._events = new EventEmitter(); internal.addObject(this); } @@ -581,13 +580,15 @@ class Sensor extends Options { 'Sensor.open() expects a streamProfile object or an array of streamProfile objects'); } if (Array.isArray(streamProfile) && streamProfile.length > 0) { + let cxxStreamProfiles = []; for (let i = 0; i < streamProfile.length; i++) { if (!(streamProfile[i] instanceof StreamProfile)) { throw new TypeError( 'Sensor.open() expects a streamProfile object or an array of streamProfile objects'); // eslint-disable-line } + cxxStreamProfiles.push(streamProfile[i].cxxProfile); } - this.cxxSensor.openMultipleStream(streamProfile); + this.cxxSensor.openMultipleStream(cxxStreamProfiles); } else { if (!(streamProfile instanceof StreamProfile)) { throw new TypeError( @@ -1053,7 +1054,10 @@ class Context { * @return {PlaybackDevice} */ loadDevice(file) { - return new PlaybackDevice(this.cxxCtx.loadDeviceFile(file)); + if (arguments.length === 0 || !isString(file)) { + throw new TypeError('Context.loadDevice expects a string argument'); + } + return new PlaybackDevice(this.cxxCtx.loadDeviceFile(file), file); } /** @@ -1062,7 +1066,10 @@ class Context { * @param {String} file The file name that was loaded to create the playback device */ unloadDevice(file) { - // TODO (Shaoting) support this method + if (arguments.length === 0 || !isString(file)) { + throw new TypeError('Context.unloadDevice expects a string argument'); + } + this.cxxCtx.unloadDeviceFile(file); } } @@ -1112,17 +1119,247 @@ class PlaybackContext extends Context { } } -class RecordDevice extends Device { - constructor(file, cxxDevice) { - super(cxxDevice); - this.file = file; +/** + * This class provides the ability to record a live session of streaming to a file + * Here is an examples: + *
+ * let ctx = new rs2.Context();
+ * let dev = ctx.queryDevices().devices[0];
+ * // record to file record.bag
+ * let recorder = new rs2.RecorderDevice('record.bag', dev);
+ * let sensors = recorder.querySensors();
+ * let sensor = sensors[0];
+ * let profiles = sensor.getStreamProfiles();
+ *
+ * for (let i =0; i < profiles.length; i++) {
+ * if (profiles[i].streamType === rs2.stream.STREAM_DEPTH &&
+ * profiles[i].fps === 30 &&
+ * profiles[i].width === 640 &&
+ * profiles[i].height === 480 &&
+ * profiles[i].format === rs2.format.FORMAT_Z16) {
+ * sensor.open(profiles[i]);
+ * }
+ * }
+ *
+ * // record 10 frames
+ * let cnt = 0;
+ * sensor.start((frame) => {
+ * cnt++;
+ * if (cnt === 10) {
+ * // stop recording
+ * recorder.reset();
+ * rs2.cleanup();
+ * console.log('Recorded ', cnt, ' frames');
+ * }
+ * })
+ *
+ * @extends Device
+ */
+class RecorderDevice extends Device {
+ /**
+ * @param {String} file the file name to store the recorded data
+ * @param {Device} device the actual device to be recorded
+ */
+ constructor(file, device) {
+ if (arguments.length != 2) {
+ throw new TypeError('RecorderDevice constructor expects 2 arguments');
+ }
+ if (!isString(file) || !(device instanceof Device)) {
+ throw new TypeError('Invalid argument types provided to RecorderDevice constructor');
+ }
+ super(device.cxxDev.spawnRecorderDevice(file));
+ }
+ /**
+ * Pause the recording device without stopping the actual device from streaming.
+ */
+ pause() {
+ this.cxxDev.pauseRecord();
+ }
+ /**
+ * Resume the recording
+ */
+ resume() {
+ this.cxxDev.resumeRecord();
}
}
+/**
+ * This class is used to playback the file recorded by RecorderDevice
+ * Here is an example:
+ *
+ * let ctx = new rs2.Context();
+ * // load the recorded file
+ * let dev = ctx.loadDevice('record.bag');
+ * let sensors = dev.querySensors();
+ * let sensor = sensors[0];
+ * let profiles = sensor.getStreamProfiles();
+ * let cnt = 0;
+ *
+ * // when received 'stopped' status, stop playback
+ * dev.setStatusChangedCallback((status) => {
+ * console.log('playback status: ', status);
+ * if (status.description === 'stopped') {
+ * dev.stop();
+ * ctx.unloadDevice('record.bag');
+ * rs2.cleanup();
+ * console.log('Playback ', cnt, ' frames');
+ * }
+ * });
+ *
+ * // start playback
+ * sensor.open(profiles);
+ * sensor.start((frame) => {
+ * cnt ++;
+ * });
+ *
+ * @extends Device
+ * @see [Context.loadDevice]{@link Context#loadDevice}
+ */
class PlaybackDevice extends Device {
- constructor(file, cxxDevice) {
- super(cxxDevice);
+ constructor(cxxdevice, file) {
+ super(cxxdevice);
this.file = file;
+ this._events = new EventEmitter();
+ }
+ /**
+ * Pauses the playback
+ * Calling pause() in "paused" status does nothing
+ * If pause() is called while playback status is "playing" or "stopped", the playback will not
+ * play until resume() is called
+ * @return {undefined}
+ */
+ pause() {
+ this.cxxDev.pausePlayback();
+ }
+ /**
+ * Resumes the playback
+ * Calling resume() while playback status is "playing" or "stopped" does nothing
+ * @return {undefined}
+ */
+ resume() {
+ this.cxxDev.resumePlayback();
+ }
+ /**
+ * Stops playback
+ * @return {undefined}
+ */
+ stop() {
+ this.cxxDev.stopPlayback();
+ }
+ /**
+ * Retrieves the name of the playback file
+ * @return {String}
+ */
+ get fileName() {
+ return this.file;
+ }
+ /**
+ * Retrieves the current position of the playback in the file in terms of time. Unit is
+ * millisecond
+ * @return {Integer}
+ */
+ get position() {
+ return this.cxxDev.getPosition();
+ }
+ /**
+ * Retrieves the total duration of the file, unit is millisecond.
+ * @return {Integer}
+ */
+ get duration() {
+ return this.cxxDev.getDuration();
+ }
+ /**
+ * Sets the playback to a specified time point of the played data
+ * @param {time} time the target time to seek to, unit is millisecond
+ * @return {undefined}
+ */
+ seek(time) {
+ if (arguments.length === 0 || !isNumber(time)) {
+ throw new TypeError('PlaybackDevice.seek(time) expects a number argument');
+ }
+ this.cxxDev.seek(time);
+ }
+ /**
+ * Indicates if playback is in real time mode or non real time
+ * In real time mode, playback will play the same way the file was recorded. If the application
+ * takes too long to handle the callback, frames may be dropped.
+ * In non real time mode, playback will wait for each callback to finish handling the data before
+ * reading the next frame. In this mode no frames will be dropped, and the application controls
+ * the frame rate of the playback (according to the callback handler duration).
+ * @return {Boolean}
+ */
+ get isRealTime() {
+ return this.cxxDev.isRealTime();
+ }
+ /**
+ * Set the playback to work in real time or non real time
+ * @param {boolean} val whether real time mode is used
+ * @return {undefined}
+ */
+ set isRealTime(val) {
+ if (arguments.length === 0 || (typeof val !== 'boolean')) {
+ throw new TypeError('PlaybackDevice.isRealTime(val) expects a boolean argument');
+ }
+ this.cxxDev.setIsRealTime(val);
+ }
+ /**
+ * Set the playing speed
+ * @param {Float} speed indicates a multiplication of the speed to play (e.g: 1 = normal,
+ * 0.5 half normal speed)
+ */
+ setPlaybackSpeed(speed) {
+ if (arguments.length === 0 || !isNumber(speed)) {
+ throw new TypeError('PlaybackDevice.setPlaybackSpeed(speed) expects a number argument');
+ }
+ this.cxxDev.setPlaybackSpeed(speed);
+ }
+
+ /**
+ * @typedef {Object} PlaybackStatusObject
+ * @property {Integer} status - The status of the notification, see {@link playback_status}
+ * for details
+ * @property {String} description - The human readable literal description of the status
+ */
+
+ /**
+ * This callback is called when the status of the playback device changed
+ * @callback StatusChangedCallback
+ * @param {PlaybackStatusObject} status
+ *
+ * @see [PlaybackDevice.setStatusChangedCallback]{@link PlaybackDevice#setStatusChangedCallback}
+ */
+
+ /**
+ * Returns the current state of the playback device
+ * @return {PlaybackStatusObject}
+ */
+ get currentStatus() {
+ let cxxStatus = this.cxxDev.getCurrentStatus();
+ if (!cxxStatus) {
+ return undefined;
+ }
+ return {status: cxxStatus, description: playback_status.playbackStatusToString(cxxStatus)};
+ }
+
+ /**
+ * Register a callback to receive the playback device's status changes
+ * @param {StatusChangedCallback} callback the callback method
+ * @return {undefined}
+ */
+ setStatusChangedCallback(callback) {
+ if (arguments.length === 0) {
+ throw new TypeError('PlaybackDevice.setStatusChangedCallback expects an argument as callback'); // eslint-disable-line
+ }
+ this._events.on('status-changed', (status) => {
+ callback({status: status, description: playback_status.playbackStatusToString(status)});
+ });
+ let inst = this;
+ if (!this.cxxDev.statusChangedCallback) {
+ this.cxxDev.statusChangedCallback = (status) => {
+ inst._events.emit('status-changed', status);
+ };
+ this.cxxDev.setStatusChangedCallbackMethodName('statusChangedCallback');
+ }
}
}
@@ -4291,6 +4528,82 @@ const visual_preset = {
VISUAL_PRESET_COUNT: RS2.RS2_VISUAL_PRESET_COUNT,
};
+
+const playback_status = {
+ /**
+ * String literal of 'unknown'
.
Unknown state
+ *
Equivalent to its uppercase counterpart
+ */
+ playback_status_unknown: 'unknown',
+ /**
+ * String literal of 'playing'
.
One or more sensors were
+ * started, playback is reading and raising data
+ *
Equivalent to its uppercase counterpart
+ */
+ playback_status_playing: 'playing',
+ /**
+ * String literal of 'paused'
.
One or more sensors were
+ * started, but playback paused reading and paused raising data
+ *
Equivalent to its uppercase counterpart
+ */
+ playback_status_paused: 'paused',
+ /**
+ * String literal of 'stopped'
.
All sensors were stopped, or playback has
+ * ended (all data was read). This is the initial playback status
+ *
Equivalent to its uppercase counterpart
+ */
+ playback_status_stopped: 'stopped',
+ /**
+ * Unknown state
+ */
+ PLAYBACK_STATUS_UNKNOWN: RS2.RS2_PLAYBACK_STATUS_UNKNOWN,
+ /**
+ * One or more sensors were started, playback is reading and raising data
+ */
+ PLAYBACK_STATUS_PLAYING: RS2.RS2_PLAYBACK_STATUS_PLAYING,
+ /**
+ * One or more sensors were started, but playback paused reading and paused raising dat
+ */
+ PLAYBACK_STATUS_PAUSED: RS2.RS2_PLAYBACK_STATUS_PAUSED,
+ /**
+ * All sensors were stopped, or playback has ended (all data was read). This is the initial
+ * playback statu
+ */
+ PLAYBACK_STATUS_STOPPED: RS2.RS2_PLAYBACK_STATUS_STOPPED,
+ /**
+ * Number of enumeration values. Not a valid input: intended to be used in for-loops.
+ * @type {Integer}
+ */
+ PLAYBACK_STATUS_COUNT: RS2.RS2_PLAYBACK_STATUS_COUNT,
+ /**
+ * Get the string representation out of the integer playback_status type
+ * @param {Integer} status the playback_status type
+ * @return {String}
+ */
+ playbackStatusToString: function(status) {
+ if (arguments.length !== 1) {
+ throw new TypeError('playback_status.playbackStatusToString() expects 1 argument');
+ }
+ let i = checkStringNumber(arguments[0],
+ this.PLAYBACK_STATUS_UNKNOWN, this.PLAYBACK_STATUS_COUNT,
+ playbackStatus2Int,
+ 'playback_status.playbackStatusToString() expects a number or string as the 1st argument', // eslint-disable-line
+ 'playback_status.playbackStatusToString() expects a valid value as the 1st argument');
+ switch (i) {
+ case this.PLAYBACK_STATUS_UNKNOWN:
+ return this.playback_status_unknown;
+ case this.PLAYBACK_STATUS_PLAYING:
+ return this.playback_status_playing;
+ case this.PLAYBACK_STATUS_PAUSED:
+ return this.playback_status_paused;
+ case this.PLAYBACK_STATUS_STOPPED:
+ return this.playback_status_stopped;
+ default:
+ throw new TypeError('playback_status.playbackStatusToString() expects a valid value as the 1st argument'); // eslint-disable-line
+ }
+ },
+};
+
// e.g. str2Int('enable_motion_correction', 'option')
function str2Int(str, category) {
const name = 'RS2_' + category.toUpperCase() + '_' + str.toUpperCase().replace(/-/g, '_');
@@ -4298,41 +4611,43 @@ function str2Int(str, category) {
}
function stream2Int(str) {
- return str2Int(str, 'stream');
+ return str2Int(str, 'stream');
}
function format2Int(str) {
- return str2Int(str, 'format');
+ return str2Int(str, 'format');
}
function option2Int(str) {
- return str2Int(str, 'option');
+ return str2Int(str, 'option');
}
function cameraInfo2Int(str) {
- return str2Int(str, 'camera_info');
+ return str2Int(str, 'camera_info');
}
function recordingMode2Int(str) {
- return str2Int(str, 'recording_mode');
+ return str2Int(str, 'recording_mode');
}
function timestampDomain2Int(str) {
- return str2Int(str, 'timestamp_domain');
+ return str2Int(str, 'timestamp_domain');
}
function NotificationCategory2Int(str) {
- return str2Int(str, 'notification_category');
+ return str2Int(str, 'notification_category');
}
function logSeverity2Int(str) {
- return str2Int(str, 'log_severity');
+ return str2Int(str, 'log_severity');
}
function distortion2Int(str) {
- return str2Int(str, 'distortion');
+ return str2Int(str, 'distortion');
}
function frameMetadata2Int(str) {
- return str2Int(str, 'frame_metadata');
+ return str2Int(str, 'frame_metadata');
}
function visualPreset2Int(str) {
- return str2Int(str, 'visual_preset');
+ return str2Int(str, 'visual_preset');
+}
+function playbackStatus2Int(str) {
+ return str2Int(str, 'playback_status');
}
-
function isArrayBuffer(value) {
- return value && value instanceof ArrayBuffer && value.byteLength !== undefined;
+ return value && (value instanceof ArrayBuffer) && (value.byteLength !== undefined);
}
const constants = {
@@ -4378,6 +4693,8 @@ module.exports = {
PointCloud: PointCloud,
Points: Points,
Syncer: Syncer,
+ RecorderDevice: RecorderDevice,
+ PlaybackDevice: PlaybackDevice,
stream: stream,
format: format,
@@ -4390,6 +4707,7 @@ module.exports = {
distortion: distortion,
frame_metadata: frame_metadata,
visual_preset: visual_preset,
+ playback_status: playback_status,
util: util,
internal: internal,
diff --git a/wrappers/nodejs/package-lock.json b/wrappers/nodejs/package-lock.json
index 15cfb7b66b..2dc674703a 100644
--- a/wrappers/nodejs/package-lock.json
+++ b/wrappers/nodejs/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "node-librealsense",
- "version": "0.282.1",
+ "version": "0.282.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/wrappers/nodejs/package.json b/wrappers/nodejs/package.json
index a99227ee7c..f4029156b7 100644
--- a/wrappers/nodejs/package.json
+++ b/wrappers/nodejs/package.json
@@ -1,6 +1,6 @@
{
"name": "node-librealsense",
- "version": "0.282.1",
+ "version": "0.282.2",
"description": "Node.js API for IntelĀ® RealSenseā¢ SDK 2.0",
"main": "index.js",
"directories": {
diff --git a/wrappers/nodejs/src/addon.cpp b/wrappers/nodejs/src/addon.cpp
index fa811a1679..6a253e0278 100644
--- a/wrappers/nodejs/src/addon.cpp
+++ b/wrappers/nodejs/src/addon.cpp
@@ -10,17 +10,38 @@
#include
#include
+#include
#include
#include
#include
class MainThreadCallbackInfo {
public:
- MainThreadCallbackInfo() {}
- virtual ~MainThreadCallbackInfo() {}
+ MainThreadCallbackInfo() : consumed_(false) {
+ pending_infos_.push_back(this);
+ }
+ virtual ~MainThreadCallbackInfo() {
+ pending_infos_.erase(
+ std::find(pending_infos_.begin(), pending_infos_.end(), this));
+ }
virtual void Run() {}
+ virtual void Release() {}
+ void SetConsumed() { consumed_ = true; }
+ static bool InfoExist(MainThreadCallbackInfo* info) {
+ auto result = std::find(pending_infos_.begin(), pending_infos_.end(), info);
+ return (result != pending_infos_.end());
+ }
+ static void ReleasePendingInfos() {
+ while (pending_infos_.size()) { delete *(pending_infos_.begin()); }
+ }
+
+ protected:
+ static std::list pending_infos_;
+ bool consumed_;
};
+std::list MainThreadCallbackInfo::pending_infos_;
+
class MainThreadCallback {
public:
static void Init() {
@@ -31,6 +52,7 @@ class MainThreadCallback {
if (singleton_) {
delete singleton_;
singleton_ = nullptr;
+ MainThreadCallbackInfo::ReleasePendingInfos();
}
}
~MainThreadCallback() {
@@ -53,12 +75,16 @@ class MainThreadCallback {
}
static void AsyncProc(uv_async_t* async) {
- if (async->data) {
- MainThreadCallbackInfo* info =
- reinterpret_cast(async->data);
- info->Run();
- delete info;
- }
+ if (!(async->data))
+ return;
+
+ MainThreadCallbackInfo* info =
+ reinterpret_cast(async->data);
+ info->Run();
+ // As the above info->Run() enters js world and during that, any code
+ // such as cleanup() could be called to release everything. So this info
+ // may has been released, we need to check before releasing it.
+ if (MainThreadCallbackInfo::InfoExist(info)) delete info;
}
static MainThreadCallback* singleton_;
uv_async_t* async_;
@@ -1070,8 +1096,16 @@ class FrameCallbackInfo : public MainThreadCallbackInfo {
public:
FrameCallbackInfo(rs2_frame* frame, void* data) :
frame_(frame), sensor_(static_cast(data)) {}
- virtual ~FrameCallbackInfo() {}
+ virtual ~FrameCallbackInfo() { if (!consumed_) Release(); }
virtual void Run();
+ virtual void Release() {
+ if (frame_) {
+ rs2_release_frame(frame_);
+ frame_ = nullptr;
+ }
+ }
+
+ private:
rs2_frame* frame_;
RSSensor* sensor_;
};
@@ -1087,6 +1121,8 @@ class NotificationCallbackInfo : public MainThreadCallbackInfo {
category_(category), sensor_(s) {}
virtual ~NotificationCallbackInfo() {}
virtual void Run();
+
+ private:
const char* desc_;
rs2_time_t time_;
rs2_log_severity severity_;
@@ -1154,6 +1190,33 @@ class FrameCallbackForProcessingBlock : public rs2_frame_callback {
rs2_error* error_;
};
+class PlaybackStatusCallbackInfo : public MainThreadCallbackInfo {
+ public:
+ PlaybackStatusCallbackInfo(rs2_playback_status status, RSDevice* dev) :
+ status_(status), dev_(dev), error_(nullptr) {}
+ virtual ~PlaybackStatusCallbackInfo() {}
+ virtual void Run();
+
+ private:
+ rs2_playback_status status_;
+ RSDevice* dev_;
+ rs2_error* error_;
+};
+
+class PlaybackStatusCallback : public rs2_playback_status_changed_callback {
+ public:
+ explicit PlaybackStatusCallback(RSDevice* dev) : error_(nullptr), dev_(dev) {}
+ void on_playback_status_changed(rs2_playback_status status) override {
+ MainThreadCallback::NotifyMainThread(new PlaybackStatusCallbackInfo(status,
+ dev_));
+ }
+ void release() override { delete this; }
+
+ private:
+ rs2_error* error_;
+ RSDevice* dev_;
+};
+
class StreamProfileExtrator {
public:
explicit StreamProfileExtrator(const rs2_stream_profile* profile) {
@@ -1952,6 +2015,11 @@ void RSSensor::RegisterNotificationCallbackMethod() {
class RSDevice : public Nan::ObjectWrap {
public:
+ enum DeviceType {
+ kNormalDevice = 0,
+ kRecorderDevice,
+ kPlaybackDevice,
+ };
static void Init(v8::Local exports) {
v8::Local tpl = Nan::New(New);
tpl->SetClassName(Nan::New("RSDevice").ToLocalChecked());
@@ -1963,12 +2031,32 @@ class RSDevice : public Nan::ObjectWrap {
Nan::SetPrototypeMethod(tpl, "reset", Reset);
Nan::SetPrototypeMethod(tpl, "querySensors", QuerySensors);
Nan::SetPrototypeMethod(tpl, "triggerErrorForTest", TriggerErrorForTest);
+ Nan::SetPrototypeMethod(tpl, "spawnRecorderDevice", SpawnRecorderDevice);
+
+ // Methods for record
+ Nan::SetPrototypeMethod(tpl, "pauseRecord", PauseRecord);
+ Nan::SetPrototypeMethod(tpl, "resumeRecord", ResumeRecord);
+
+ // Methods for playback
+ Nan::SetPrototypeMethod(tpl, "pausePlayback", PausePlayback);
+ Nan::SetPrototypeMethod(tpl, "resumePlayback", ResumePlayback);
+ Nan::SetPrototypeMethod(tpl, "stopPlayback", StopPlayback);
+ Nan::SetPrototypeMethod(tpl, "getPosition", GetPosition);
+ Nan::SetPrototypeMethod(tpl, "getDuration", GetDuration);
+ Nan::SetPrototypeMethod(tpl, "seek", Seek);
+ Nan::SetPrototypeMethod(tpl, "isRealTime", IsRealTime);
+ Nan::SetPrototypeMethod(tpl, "setIsRealTime", SetIsRealTime);
+ Nan::SetPrototypeMethod(tpl, "setPlaybackSpeed", SetPlaybackSpeed);
+ Nan::SetPrototypeMethod(tpl, "getCurrentStatus", GetCurrentStatus);
+ Nan::SetPrototypeMethod(tpl, "setStatusChangedCallbackMethodName",
+ SetStatusChangedCallbackMethodName);
constructor_.Reset(tpl->GetFunction());
exports->Set(Nan::New("RSDevice").ToLocalChecked(), tpl->GetFunction());
}
- static v8::Local NewInstance(rs2_device* dev) {
+ static v8::Local NewInstance(rs2_device* dev,
+ DeviceType type = kNormalDevice) {
Nan::EscapableHandleScope scope;
v8::Local cons = Nan::New(constructor_);
@@ -1980,12 +2068,14 @@ class RSDevice : public Nan::ObjectWrap {
auto me = Nan::ObjectWrap::Unwrap(instance);
me->dev_ = dev;
+ me->type_ = type;
return scope.Escape(instance);
}
private:
- RSDevice() : dev_(nullptr), error_(nullptr) {}
+ explicit RSDevice(DeviceType type = kNormalDevice) : dev_(nullptr),
+ error_(nullptr), type_(type) {}
~RSDevice() {
DestroyMe();
@@ -2047,23 +2137,24 @@ class RSDevice : public Nan::ObjectWrap {
}
static NAN_METHOD(QuerySensors) {
+ info.GetReturnValue().Set(Nan::Undefined());
auto me = Nan::ObjectWrap::Unwrap(info.Holder());
- if (me) {
- rs2_sensor_list* list = rs2_query_sensors(me->dev_, &me->error_);
- if (list) {
- auto size = rs2_get_sensors_count(list, &me->error_);
- if (size) {
- v8::Local array = Nan::New();
- for (int32_t i = 0; i < size; i++) {
- rs2_sensor* sensor = rs2_create_sensor(list, i, &me->error_);
- array->Set(i, RSSensor::NewInstance(sensor));
- }
- info.GetReturnValue().Set(array);
- return;
- }
- }
+ if (!me) return;
+
+ std::shared_ptr list(
+ rs2_query_sensors(me->dev_, &me->error_),
+ rs2_delete_sensor_list);
+ if (!list) return;
+
+ auto size = rs2_get_sensors_count(list.get(), &me->error_);
+ if (!size) return;
+
+ v8::Local array = Nan::New();
+ for (int32_t i = 0; i < size; i++) {
+ rs2_sensor* sensor = rs2_create_sensor(list.get(), i, &me->error_);
+ array->Set(i, RSSensor::NewInstance(sensor));
}
- info.GetReturnValue().Set(Nan::Undefined());
+ info.GetReturnValue().Set(array);
}
static NAN_METHOD(TriggerErrorForTest) {
@@ -2081,21 +2172,142 @@ class RSDevice : public Nan::ObjectWrap {
info.GetReturnValue().Set(Nan::Undefined());
}
+ static NAN_METHOD(SpawnRecorderDevice) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ v8::String::Utf8Value file(info[0]->ToString());
+ auto dev = rs2_create_record_device(me->dev_, *file, &me->error_);
+ auto obj = RSDevice::NewInstance(dev, kRecorderDevice);
+ info.GetReturnValue().Set(obj);
+ }
+
+ static NAN_METHOD(PauseRecord) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ if (me) rs2_record_device_pause(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::Undefined());
+ }
+
+ static NAN_METHOD(ResumeRecord) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ if (me) rs2_record_device_resume(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::Undefined());
+ }
+
+ static NAN_METHOD(PausePlayback) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ if (me) rs2_playback_device_pause(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::Undefined());
+ }
+
+ static NAN_METHOD(ResumePlayback) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ if (me) rs2_playback_device_resume(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::Undefined());
+ }
+
+ static NAN_METHOD(StopPlayback) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ if (me) rs2_playback_device_stop(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::Undefined());
+ }
+
+ static NAN_METHOD(GetPosition) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto pos = static_cast(rs2_playback_get_position(
+ me->dev_, &me->error_)/1000000);
+ info.GetReturnValue().Set(Nan::New(pos));
+ }
+
+ static NAN_METHOD(GetDuration) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto duration = static_cast(
+ rs2_playback_get_duration(me->dev_, &me->error_)/1000000);
+ info.GetReturnValue().Set(Nan::New(duration));
+ }
+
+ static NAN_METHOD(Seek) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ uint64_t time = info[0]->IntegerValue();
+ rs2_playback_seek(me->dev_, time*1000000, &me->error_);
+ }
+
+ static NAN_METHOD(IsRealTime) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto val = rs2_playback_device_is_real_time(me->dev_, &me->error_);
+ info.GetReturnValue().Set(val ? Nan::True() : Nan::False());
+ }
+
+ static NAN_METHOD(SetIsRealTime) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto val = info[0]->BooleanValue();
+ rs2_playback_device_set_real_time(me->dev_, val, &me->error_);
+ }
+
+ static NAN_METHOD(SetPlaybackSpeed) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto speed = info[0]->NumberValue();
+ rs2_playback_device_set_playback_speed(me->dev_, speed, &me->error_);
+ }
+
+ static NAN_METHOD(GetCurrentStatus) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto status = rs2_playback_device_get_current_status(me->dev_, &me->error_);
+ info.GetReturnValue().Set(Nan::New(status));
+ }
+
+ static NAN_METHOD(SetStatusChangedCallbackMethodName) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ v8::String::Utf8Value method(info[0]->ToString());
+ me->status_changed_callback_method_name_ = std::string(*method);
+ rs2_playback_device_set_status_changed_callback(me->dev_,
+ new PlaybackStatusCallback(me), &me->error_);
+ }
+
private:
static Nan::Persistent constructor_;
rs2_device* dev_;
rs2_error* error_;
+ DeviceType type_;
+ std::string status_changed_callback_method_name_;
friend class RSContext;
friend class DevicesChangedCallbackInfo;
friend class FrameCallbackInfo;
friend class RSPipeline;
friend class RSDeviceList;
friend class RSDeviceHub;
+ friend class PlaybackStatusCallbackInfo;
};
Nan::Persistent RSDevice::constructor_;
void FrameCallbackInfo::Run() {
+ SetConsumed();
Nan::HandleScope scope;
// save the rs2_frame to the sensor
sensor_->ReplaceFrame(frame_);
@@ -2104,6 +2316,7 @@ void FrameCallbackInfo::Run() {
}
void NotificationCallbackInfo::Run() {
+ SetConsumed();
Nan::HandleScope scope;
v8::Local args[1] = {
RSNotification(desc_, time_, severity_, category_).GetObject()
@@ -2112,6 +2325,14 @@ void NotificationCallbackInfo::Run() {
sensor_->notification_callback_name_.c_str(), 1, args);
}
+void PlaybackStatusCallbackInfo::Run() {
+ SetConsumed();
+ Nan::HandleScope scope;
+ v8::Local args[1] = { Nan::New(status_) };
+ Nan::MakeCallback(dev_->handle(),
+ dev_->status_changed_callback_method_name_.c_str(), 1, args);
+}
+
class RSPointCloud : public Nan::ObjectWrap {
public:
static void Init(v8::Local exports) {
@@ -2204,16 +2425,6 @@ class RSPointCloud : public Nan::ObjectWrap {
Nan::Persistent RSPointCloud::constructor_;
-// TODO(shaoting) implement playback status
-// class PlaybackStatusChangedCallback :
-// public rs2_playback_status_changed_callback {
-// virtual void on_playback_status_changed(rs2_playback_status status) {
-// // TODO(tingshao): add more logic here.
-// }
-// virtual void release() { delete this; }
-// virtual ~PlaybackStatusChangedCallback() {}
-// };
-
class RSDeviceList : public Nan::ObjectWrap {
public:
static void Init(v8::Local exports) {
@@ -2349,6 +2560,7 @@ class RSContext : public Nan::ObjectWrap {
Nan::SetPrototypeMethod(tpl, "setDevicesChangedCallback",
SetDevicesChangedCallback);
Nan::SetPrototypeMethod(tpl, "loadDeviceFile", LoadDeviceFile);
+ Nan::SetPrototypeMethod(tpl, "unloadDeviceFile", UnloadDeviceFile);
Nan::SetPrototypeMethod(tpl, "createDeviceFromSensor",
CreateDeviceFromSensor);
@@ -2465,7 +2677,7 @@ class RSContext : public Nan::ObjectWrap {
v8::String::Utf8Value value(device_file);
auto dev = rs2_context_add_device(me->ctx_, *value, &me->error_);
if (dev) {
- auto jsobj = RSDevice::NewInstance(dev);
+ auto jsobj = RSDevice::NewInstance(dev, RSDevice::kPlaybackDevice);
info.GetReturnValue().Set(jsobj);
return;
}
@@ -2473,6 +2685,16 @@ class RSContext : public Nan::ObjectWrap {
info.GetReturnValue().Set(Nan::Undefined());
}
+ static NAN_METHOD(UnloadDeviceFile) {
+ auto me = Nan::ObjectWrap::Unwrap(info.Holder());
+ info.GetReturnValue().Set(Nan::Undefined());
+ if (!me) return;
+
+ auto device_file = info[0]->ToString();
+ v8::String::Utf8Value value(device_file);
+ rs2_context_remove_device(me->ctx_, *value, &me->error_);
+ }
+
static NAN_METHOD(CreateDeviceFromSensor) {
auto sensor = Nan::ObjectWrap::Unwrap(info[0]->ToObject());
if (sensor) {
@@ -2522,8 +2744,9 @@ class DevicesChangedCallbackInfo : public MainThreadCallbackInfo {
DevicesChangedCallbackInfo(rs2_device_list* r,
rs2_device_list* a, RSContext* ctx) :
removed_(r), added_(a), ctx_(ctx) {}
- virtual ~DevicesChangedCallbackInfo() {}
+ virtual ~DevicesChangedCallbackInfo() { if (!consumed_) Release(); }
virtual void Run() {
+ SetConsumed();
Nan::HandleScope scope;
v8::Local rmlist;
v8::Local addlist;
@@ -2541,6 +2764,19 @@ class DevicesChangedCallbackInfo : public MainThreadCallbackInfo {
Nan::MakeCallback(ctx_->handle(),
ctx_->device_changed_callback_name_.c_str(), 2, args);
}
+ virtual void Release() {
+ if (removed_) {
+ rs2_delete_device_list(removed_);
+ removed_ = nullptr;
+ }
+
+ if (added_) {
+ rs2_delete_device_list(added_);
+ added_ = nullptr;
+ }
+ }
+
+ private:
rs2_device_list* removed_;
rs2_device_list* added_;
RSContext* ctx_;
@@ -3597,6 +3833,13 @@ void InitModule(v8::Local exports) {
_FORCE_SET_ENUM(RS2_RECORDING_MODE_COMPRESSED);
_FORCE_SET_ENUM(RS2_RECORDING_MODE_BEST_QUALITY);
_FORCE_SET_ENUM(RS2_RECORDING_MODE_COUNT);
+
+ // rs2_playback_status
+ _FORCE_SET_ENUM(RS2_PLAYBACK_STATUS_UNKNOWN);
+ _FORCE_SET_ENUM(RS2_PLAYBACK_STATUS_PLAYING);
+ _FORCE_SET_ENUM(RS2_PLAYBACK_STATUS_PAUSED);
+ _FORCE_SET_ENUM(RS2_PLAYBACK_STATUS_STOPPED);
+ _FORCE_SET_ENUM(RS2_PLAYBACK_STATUS_COUNT);
}
NODE_MODULE(node_librealsense, InitModule);
diff --git a/wrappers/nodejs/test/test-functional.js b/wrappers/nodejs/test/test-functional.js
index af03b35234..096ccbce0b 100644
--- a/wrappers/nodejs/test/test-functional.js
+++ b/wrappers/nodejs/test/test-functional.js
@@ -4,8 +4,9 @@
'use strict';
-/* global describe, it, before, after */
+/* global describe, it, before, after, afterEach */
const assert = require('assert');
+const fs = require('fs');
let rs2;
try {
rs2 = require('node-librealsense');
@@ -640,3 +641,115 @@ describe(('DeviceHub test'), function() {
dev.destroy();
});
});
+
+describe(('record & playback test'), function() {
+ let fileName = 'ut-record.bag';
+
+ afterEach(() => {
+ rs2.cleanup();
+ });
+
+ function startRecording(file, cnt, callback) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ let ctx = new rs2.Context();
+ let dev = ctx.queryDevices().devices[0];
+ let recorder = new rs2.RecorderDevice(file, dev);
+ let sensors = recorder.querySensors();
+ let sensor = sensors[0];
+ let profiles = sensor.getStreamProfiles();
+ for (let i =0; i < profiles.length; i++) {
+ if (profiles[i].streamType === rs2.stream.STREAM_DEPTH &&
+ profiles[i].fps === 30 &&
+ profiles[i].width === 640 &&
+ profiles[i].height === 480 &&
+ profiles[i].format === rs2.format.FORMAT_Z16) {
+ sensor.open(profiles[i]);
+ }
+ }
+ let counter = 0;
+ sensor.start((frame) => {
+ if (callback) {
+ callback(recorder, counter);
+ }
+ counter++;
+ if (counter === cnt) {
+ recorder.reset();
+ rs2.cleanup();
+ resolve();
+ }
+ });
+ }, 2000);
+ });
+ }
+
+ function startPlayback(file, callback) {
+ return new Promise((resolve, reject) => {
+ let ctx = new rs2.Context();
+ let dev = ctx.loadDevice(file);
+ let sensors = dev.querySensors();
+ let sensor = sensors[0];
+ let profiles = sensor.getStreamProfiles();
+ let cnt = 0;
+
+ dev.setStatusChangedCallback((status) => {
+ callback(dev, status, cnt);
+ if (status.description === 'stopped') {
+ console.log('stopped');
+ dev.stop();
+ ctx.unloadDevice(file);
+ rs2.cleanup();
+ resolve();
+ }
+ });
+ sensor.open(profiles);
+ sensor.start((frame) => {
+ cnt++;
+ });
+ });
+ }
+
+ it('record test', () => {
+ return new Promise((resolve, reject) => {
+ startRecording(fileName, 1, null).then(() => {
+ assert.equal(fs.existsSync(fileName), true);
+ fs.unlinkSync(fileName);
+ resolve();
+ });
+ });
+ }).timeout(5000);
+
+ it('pause/resume test', () => {
+ return new Promise((resolve, reject) => {
+ startRecording(fileName, 2, (recorder, cnt) => {
+ if (cnt === 1) {
+ recorder.pause();
+ recorder.resume();
+ }
+ }).then(() => {
+ assert.equal(fs.existsSync(fileName), true);
+ fs.unlinkSync(fileName);
+ resolve();
+ });
+ });
+ }).timeout(5000);
+
+ it('playback test', () => {
+ return new Promise((resolve, reject) => {
+ startRecording(fileName, 1, null).then(() => {
+ assert.equal(fs.existsSync(fileName), true);
+ return startPlayback(fileName, (playbackDev, status) => {
+ if (status.description === 'stopped') {
+ resolve();
+ } else if (status.description === 'playing') {
+ assert.equal(playbackDev.fileName, 'ut-record.bag');
+ assert.equal(typeof playbackDev.duration, 'number');
+ assert.equal(typeof playbackDev.position, 'number');
+ assert.equal(typeof playbackDev.isRealTime, 'boolean');
+ assert.equal(playbackDev.currentStatus.description, 'playing');
+ }
+ });
+ });
+ });
+ }).timeout(5000);
+});