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); +});