noobs 0.0.89 → 0.0.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/noobs.node CHANGED
Binary file
package/index.d.ts CHANGED
@@ -136,8 +136,10 @@ export type ObsProperty =
136
136
  | ObsGenericProperty;
137
137
 
138
138
  export type Signal = {
139
+ type: string; // Either "output" or "volmeter".
139
140
  id: string; // Signal identifier, e.g. "stop"
140
141
  code: number; // 0 for success, other values for errors
142
+ value?: number; // Currently only used for volmeters.
141
143
  };
142
144
 
143
145
  export type SceneItemPosition = {
@@ -162,43 +164,50 @@ interface Noobs {
162
164
 
163
165
  Shutdown(): void;
164
166
 
165
- // Recording functions
167
+ // Recording functions.
166
168
  SetBuffering(buffering: boolean): void; // In buffering mode, the recording is stored in memory and can be converted to a file later.
167
169
  StartBuffer(): void;
168
170
  StartRecording(offset: number): void;
169
171
  StopRecording(): void;
172
+ ForceStopRecording(): void;
170
173
  GetLastRecording(): string;
171
174
  SetRecordingDir(recordingPath: string): void;
175
+ ResetVideoContext(fps: number, width: number, height: number): void;
172
176
 
173
- // Source management functions
177
+ // Encoder functions.
178
+ ListVideoEncoders(): string[]; // Returns a list of available video encoders.
179
+ SetVideoEncoder(id: string, settings: ObsData): void; // Create the video encoder to use.
180
+
181
+ // Source management functions.
174
182
  CreateSource(name: string, type: string): void;
175
183
  DeleteSource(name: string): void;
176
184
  GetSourceSettings(name: string): ObsData;
177
185
  SetSourceSettings(name: string, settings: ObsData): void;
178
186
  GetSourceProperties(name: string): ObsProperty[];
179
187
 
180
- // Scene management functions
188
+ // Audio source management functions.
189
+ SetMuteAudioInputs(mute: boolean): void; // Mute or unmute all audio inputs.
190
+ SetInputVolume(volume: number): void; // Set the volume for all audio inputs sources.
191
+ SetOutputVolume(volume: number): void; // Set the volume for all audio outputs sources.
192
+ SetProcessVolume(volume: number): void; // Set the volume for all audio process sources.
193
+
194
+ SetVolmeterEnabled(enabled: boolean): void; // Enable or disable the volume meter.
195
+
196
+ // Scene management functions.
181
197
  AddSourceToScene(sourceName: string): void;
182
198
  RemoveSourceFromScene(sourceName: string): void;
183
199
  GetSourcePos(name: string): SceneItemPosition & SourceDimensions;
184
200
  SetSourcePos(name: string, pos: SceneItemPosition): void;
185
201
  // TODO: Cropping?
186
202
 
187
- // Preview functions
203
+ // Preview functions.
188
204
  InitPreview(hwnd: Buffer): void;
189
- ShowPreview(x: number, y: number, width: number, height: number): void;
205
+ ConfigurePreview(x: number, y: number, width: number, height: number): void;
206
+ ShowPreview(): void;
190
207
  HidePreview(): void;
191
- GetPreviewScaleFactor(): number;
192
- /**
193
- * Draws a red border around source preview
194
- * @param enabled
195
- */
208
+ DisablePreview(): void;
209
+ GetPreviewInfo(): { canvasWidth: number; canvasHeight: number; previewWidth: number; previewHeight: number };
196
210
  SetDrawSourceOutline(enabled: boolean): void;
197
- /**
198
- * Gets the current state of the preview outline, a red border around the source preview
199
- *
200
- * Default: *false*
201
- */
202
211
  GetDrawSourceOutlineEnabled(): boolean;
203
212
  }
204
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noobs",
3
- "version": "0.0.89",
3
+ "version": "0.0.115",
4
4
  "description": "A native Node.js addon with libobs bindings for Warcraft Recorder.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/src/main.cpp CHANGED
@@ -54,6 +54,75 @@ Napi::Value ObsSetRecordingDir(const Napi::CallbackInfo& info) {
54
54
  return info.Env().Undefined();
55
55
  }
56
56
 
57
+ Napi::Value ObsResetVideoContext(const Napi::CallbackInfo& info) {
58
+ if (!obs) {
59
+ blog(LOG_ERROR, "ObsResetVideoContext called but obs is not initialized");
60
+ throw std::runtime_error("Obs not initialized");
61
+ }
62
+
63
+ bool valid = info.Length() == 3 && info[0].IsNumber() && info[1].IsNumber() && info[2].IsNumber();
64
+
65
+ if (!valid) {
66
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsResetVideo").ThrowAsJavaScriptException();
67
+ return info.Env().Undefined();
68
+ }
69
+
70
+ int fps = info[0].As<Napi::Number>().Int32Value();
71
+ int width = info[1].As<Napi::Number>().Int32Value();
72
+ int height = info[2].As<Napi::Number>().Int32Value();
73
+
74
+ obs->setVideoContext(fps, width, height);
75
+ return info.Env().Undefined();
76
+ }
77
+
78
+ Napi::Value ObsListVideoEncoders(const Napi::CallbackInfo& info) {
79
+ if (!obs) {
80
+ blog(LOG_ERROR, "ObsListVideoEncoders called but obs is not initialized");
81
+ throw std::runtime_error("Obs not initialized");
82
+ }
83
+
84
+ bool valid = info.Length() == 0;
85
+
86
+ if (!valid) {
87
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsListVideoEncoders").ThrowAsJavaScriptException();
88
+ return info.Env().Undefined();
89
+ }
90
+
91
+ auto encoders = obs->listAvailableVideoEncoders();
92
+ Napi::Array result = Napi::Array::New(info.Env(), encoders.size());
93
+
94
+ for (size_t i = 0; i < encoders.size(); ++i) {
95
+ result[i] = Napi::String::New(info.Env(), encoders[i]);
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ Napi::Value ObsSetVideoEncoder(const Napi::CallbackInfo& info) {
102
+ if (!obs) {
103
+ blog(LOG_ERROR, "ObsSetVideoEncoder called but obs is not initialized");
104
+ throw std::runtime_error("Obs not initialized");
105
+ }
106
+
107
+ bool valid = info.Length() == 2 &&
108
+ info[0].IsString() && // Encoder ID
109
+ info[1].IsObject(); // Settings object
110
+
111
+ if (!valid) {
112
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetVideoEncoder").ThrowAsJavaScriptException();
113
+ return info.Env().Undefined();
114
+ }
115
+
116
+ std::string id = info[0].As<Napi::String>().Utf8Value();
117
+ Napi::Object obj = info[1].As<Napi::Object>();
118
+
119
+ obs_data_t* settings = napi_to_data(obj);
120
+ obs->setVideoEncoder(id, settings);
121
+ obs_data_release(settings);
122
+
123
+ return info.Env().Undefined();
124
+ }
125
+
57
126
  Napi::Value ObsSetBuffering(const Napi::CallbackInfo& info) {
58
127
  blog(LOG_INFO, "ObsSetBuffering called");
59
128
 
@@ -118,6 +187,16 @@ Napi::Value ObsStopRecording(const Napi::CallbackInfo& info) {
118
187
  return info.Env().Undefined();
119
188
  }
120
189
 
190
+ Napi::Value ObsForceStopRecording(const Napi::CallbackInfo& info) {
191
+ if (!obs) {
192
+ blog(LOG_ERROR, "ObsForceStopRecording called but obs is not initialized");
193
+ throw std::runtime_error("Obs not initialized");
194
+ }
195
+
196
+ obs->forceStopRecording();
197
+ return info.Env().Undefined();
198
+ }
199
+
121
200
  Napi::Value ObsGetLastRecording(const Napi::CallbackInfo& info) {
122
201
  if (!obs) {
123
202
  blog(LOG_ERROR, "ObsGetLastRecording called but obs is not initialized");
@@ -155,11 +234,11 @@ Napi::Value ObsInitPreview(const Napi::CallbackInfo& info) {
155
234
  return info.Env().Undefined();
156
235
  }
157
236
 
158
- Napi::Value ObsShowPreview(const Napi::CallbackInfo& info) {
159
- blog(LOG_INFO, "ObsShowPreview called");
237
+ Napi::Value ObsConfigurePreview(const Napi::CallbackInfo& info) {
238
+ blog(LOG_INFO, "ObsConfigurePreview called");
160
239
 
161
240
  if (!obs) {
162
- blog(LOG_ERROR, "ObsShowPreview called but obs is not initialized");
241
+ blog(LOG_ERROR, "ObsConfigurePreview called but obs is not initialized");
163
242
  throw std::runtime_error("Obs not initialized");
164
243
  }
165
244
 
@@ -170,7 +249,7 @@ Napi::Value ObsShowPreview(const Napi::CallbackInfo& info) {
170
249
  info[3].IsNumber(); // Height
171
250
 
172
251
  if (!valid) {
173
- Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsShowPreview").ThrowAsJavaScriptException();
252
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsConfigurePreview").ThrowAsJavaScriptException();
174
253
  return info.Env().Undefined();
175
254
  }
176
255
 
@@ -179,7 +258,19 @@ Napi::Value ObsShowPreview(const Napi::CallbackInfo& info) {
179
258
  int width = info[2].As<Napi::Number>().Int32Value();
180
259
  int height = info[3].As<Napi::Number>().Int32Value();
181
260
 
182
- obs->showPreview(x, y, width, height);
261
+ obs->configurePreview(x, y, width, height);
262
+ return info.Env().Undefined();
263
+ }
264
+
265
+ Napi::Value ObsShowPreview(const Napi::CallbackInfo& info) {
266
+ blog(LOG_INFO, "ObsShowPreview called");
267
+
268
+ if (!obs) {
269
+ blog(LOG_ERROR, "ObsShowPreview called but obs is not initialized");
270
+ throw std::runtime_error("Obs not initialized");
271
+ }
272
+
273
+ obs->showPreview();
183
274
  return info.Env().Undefined();
184
275
  }
185
276
 
@@ -193,14 +284,31 @@ Napi::Value ObsHidePreview(const Napi::CallbackInfo& info) {
193
284
  return info.Env().Undefined();
194
285
  }
195
286
 
196
- Napi::Value ObsGetPreviewScaleFactor(const Napi::CallbackInfo& info) {
287
+ Napi::Value ObsDisablePreview(const Napi::CallbackInfo& info) {
288
+ if (!obs) {
289
+ blog(LOG_ERROR, "ObsDisablePreview called but obs is not initialized");
290
+ throw std::runtime_error("Obs not initialized");
291
+ }
292
+
293
+ obs->disablePreview();
294
+ return info.Env().Undefined();
295
+ }
296
+
297
+ Napi::Value ObsGetPreviewInfo(const Napi::CallbackInfo& info) {
197
298
  if (!obs) {
198
- blog(LOG_ERROR, "ObsGetPreviewScaleFactor called but obs is not initialized");
299
+ blog(LOG_ERROR, "ObsGetPreviewInfo called but obs is not initialized");
199
300
  throw std::runtime_error("Obs not initialized");
200
301
  }
201
302
 
202
- float scaleFactor = obs->getPreviewScaleFactor();
203
- return Napi::Number::New(info.Env(), scaleFactor);
303
+ PreviewInfo previewInfo = obs->getPreviewInfo();
304
+
305
+ Napi::Object result = Napi::Object::New(info.Env());
306
+ result.Set("canvasWidth", Napi::Number::New(info.Env(), previewInfo.canvasWidth));
307
+ result.Set("canvasHeight", Napi::Number::New(info.Env(), previewInfo.canvasHeight));
308
+ result.Set("previewWidth", Napi::Number::New(info.Env(), previewInfo.displayWidth));
309
+ result.Set("previewHeight", Napi::Number::New(info.Env(), previewInfo.displayHeight));
310
+
311
+ return result;
204
312
  }
205
313
 
206
314
  Napi::Value ObsCreateSource(const Napi::CallbackInfo& info) {
@@ -309,6 +417,98 @@ Napi::Value ObsGetSourceProperties(const Napi::CallbackInfo& info) {
309
417
  return result;
310
418
  }
311
419
 
420
+
421
+ Napi::Value ObsSetMuteAudioInputs(const Napi::CallbackInfo& info) {
422
+ if (!obs) {
423
+ blog(LOG_ERROR, "ObsSetMuteAudioInputs called but obs is not initialized");
424
+ throw std::runtime_error("Obs not initialized");
425
+ }
426
+
427
+ bool valid = info.Length() == 1 && info[0].IsBoolean();
428
+
429
+ if (!valid) {
430
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetMuteAudioInputs").ThrowAsJavaScriptException();
431
+ return info.Env().Undefined();
432
+ }
433
+
434
+ bool mute = info[0].As<Napi::Boolean>().Value();
435
+ obs->setMuteAudioInputs(mute);
436
+ return info.Env().Undefined();
437
+ }
438
+
439
+ Napi::Value ObsSetOutputVolume(const Napi::CallbackInfo& info) {
440
+ if (!obs) {
441
+ blog(LOG_ERROR, "ObsSetOutputVolume called but obs is not initialized");
442
+ throw std::runtime_error("Obs not initialized");
443
+ }
444
+
445
+ bool valid = info.Length() == 1 && info[0].IsNumber();
446
+ float volume = info[0].As<Napi::Number>().FloatValue();
447
+
448
+ if (!valid || (volume < 0.0f || volume > 1.0f)) {
449
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetOutputVolume").ThrowAsJavaScriptException();
450
+ return info.Env().Undefined();
451
+ }
452
+
453
+ obs->setOutputVolume(volume);
454
+ return info.Env().Undefined();
455
+ }
456
+
457
+ Napi::Value ObsSetInputVolume(const Napi::CallbackInfo& info) {
458
+ if (!obs) {
459
+ blog(LOG_ERROR, "ObsSetInputVolume called but obs is not initialized");
460
+ throw std::runtime_error("Obs not initialized");
461
+ }
462
+
463
+ bool valid = info.Length() == 1 && info[0].IsNumber();
464
+ float volume = info[0].As<Napi::Number>().FloatValue();
465
+
466
+ if (!valid || (volume < 0.0f || volume > 1.0f)) {
467
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetInputVolume").ThrowAsJavaScriptException();
468
+ return info.Env().Undefined();
469
+ }
470
+
471
+ obs->setInputVolume(volume);
472
+ return info.Env().Undefined();
473
+ }
474
+
475
+ Napi::Value ObsSetProcessVolume(const Napi::CallbackInfo& info) {
476
+ if (!obs) {
477
+ blog(LOG_ERROR, "ObsSetProcessVolume called but obs is not initialized");
478
+ throw std::runtime_error("Obs not initialized");
479
+ }
480
+
481
+ bool valid = info.Length() == 1 && info[0].IsNumber();
482
+ float volume = info[0].As<Napi::Number>().FloatValue();
483
+
484
+ if (!valid || (volume < 0.0f || volume > 1.0f)) {
485
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetProcessVolume").ThrowAsJavaScriptException();
486
+ return info.Env().Undefined();
487
+ }
488
+
489
+ obs->setProcessVolume(volume);
490
+ return info.Env().Undefined();
491
+ }
492
+
493
+ Napi::Value ObsSetVolmeterEnabled(const Napi::CallbackInfo& info) {
494
+ if (!obs) {
495
+ blog(LOG_ERROR, "ObsSetVolmeterEnabled called but obs is not initialized");
496
+ throw std::runtime_error("Obs not initialized");
497
+ }
498
+
499
+ bool valid = info.Length() == 1 && info[0].IsBoolean();
500
+
501
+
502
+ if (!valid) {
503
+ Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetVolmeterEnabled").ThrowAsJavaScriptException();
504
+ return info.Env().Undefined();
505
+ }
506
+
507
+ bool enabled = info[0].As<Napi::Boolean>().Value();
508
+ obs->setVolmeterEnabled(enabled);
509
+ return info.Env().Undefined();
510
+ }
511
+
312
512
  Napi::Value ObsCreateScene(const Napi::CallbackInfo& info) {
313
513
  if (!obs) {
314
514
  blog(LOG_ERROR, "ObsCreateScene called but obs is not initialized");
@@ -447,11 +647,15 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
447
647
  exports.Set("Init", Napi::Function::New(env, ObsInit));
448
648
  exports.Set("Shutdown", Napi::Function::New(env, ObsShutdown));
449
649
  exports.Set("SetRecordingDir", Napi::Function::New(env, ObsSetRecordingDir));
650
+ exports.Set("ResetVideoContext", Napi::Function::New(env, ObsResetVideoContext));
651
+ exports.Set("ListVideoEncoders", Napi::Function::New(env, ObsListVideoEncoders));
652
+ exports.Set("SetVideoEncoder", Napi::Function::New(env, ObsSetVideoEncoder));
450
653
 
451
654
  exports.Set("SetBuffering", Napi::Function::New(env, ObsSetBuffering));
452
655
  exports.Set("StartBuffer", Napi::Function::New(env, ObsStartBuffer));
453
656
  exports.Set("StartRecording", Napi::Function::New(env, ObsStartRecording));
454
657
  exports.Set("StopRecording", Napi::Function::New(env, ObsStopRecording));
658
+ exports.Set("ForceStopRecording", Napi::Function::New(env, ObsForceStopRecording));
455
659
  exports.Set("GetLastRecording", Napi::Function::New(env, ObsGetLastRecording));
456
660
 
457
661
  exports.Set("CreateSource", Napi::Function::New(env, ObsCreateSource));
@@ -459,6 +663,13 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
459
663
  exports.Set("GetSourceSettings", Napi::Function::New(env, ObsGetSourceSettings));
460
664
  exports.Set("SetSourceSettings", Napi::Function::New(env, ObsSetSourceSettings));
461
665
  exports.Set("GetSourceProperties", Napi::Function::New(env, ObsGetSourceProperties));
666
+ exports.Set("SetMuteAudioInputs", Napi::Function::New(env, ObsSetMuteAudioInputs));
667
+
668
+ exports.Set("SetOutputVolume", Napi::Function::New(env, ObsSetOutputVolume));
669
+ exports.Set("SetInputVolume", Napi::Function::New(env, ObsSetInputVolume));
670
+ exports.Set("SetProcessVolume", Napi::Function::New(env, ObsSetProcessVolume));
671
+
672
+ exports.Set("SetVolmeterEnabled", Napi::Function::New(env, ObsSetVolmeterEnabled));
462
673
 
463
674
  exports.Set("AddSourceToScene", Napi::Function::New(env, ObsAddSourceToScene));
464
675
  exports.Set("RemoveSourceFromScene", Napi::Function::New(env, ObsRemoveSourceFromScene));
@@ -466,9 +677,11 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
466
677
  exports.Set("SetSourcePos", Napi::Function::New(env, ObsSetSourcePos));
467
678
 
468
679
  exports.Set("InitPreview", Napi::Function::New(env, ObsInitPreview));
680
+ exports.Set("ConfigurePreview", Napi::Function::New(env, ObsConfigurePreview));
469
681
  exports.Set("ShowPreview", Napi::Function::New(env, ObsShowPreview));
470
682
  exports.Set("HidePreview", Napi::Function::New(env, ObsHidePreview));
471
- exports.Set("GetPreviewScaleFactor", Napi::Function::New(env, ObsGetPreviewScaleFactor));
683
+ exports.Set("DisablePreview", Napi::Function::New(env, ObsDisablePreview));
684
+ exports.Set("GetPreviewInfo", Napi::Function::New(env, ObsGetPreviewInfo));
472
685
  exports.Set("GetDrawSourceOutlineEnabled", Napi::Function::New(env, ObsGetDrawSourceOutlineEnabled));
473
686
  exports.Set("SetDrawSourceOutline", Napi::Function::New(env, ObsSetDrawSourceOutline));
474
687
 
@@ -8,46 +8,34 @@
8
8
  #include <graphics/vec4.h>
9
9
  #include <util/platform.h>
10
10
 
11
- std::vector<std::string> ObsInterface::get_available_video_encoders()
12
- {
13
- std::vector<std::string> encoders;
14
- size_t idx = 0;
15
- const char *encoder_type;
16
-
17
- while (obs_enum_encoder_types(idx++, &encoder_type)) {
18
- bool video = obs_get_encoder_type(encoder_type) == OBS_ENCODER_VIDEO;
11
+ void call_jscb(Napi::Env env, Napi::Function cb, SignalData* sd) {
12
+ Napi::Object obj = Napi::Object::New(env);
13
+ obj.Set("type", Napi::String::New(env, sd->type));
14
+ obj.Set("id", Napi::String::New(env, sd->id));
15
+ obj.Set("code", Napi::Number::New(env, sd->code));
19
16
 
20
- if (video)
21
- encoders.emplace_back(encoder_type);
17
+ if (sd->value.has_value()) {
18
+ obj.Set("value", Napi::Number::New(env, sd->value.value()));
22
19
  }
23
20
 
24
- return encoders;
21
+ cb.Call({ obj });
22
+ delete sd;
25
23
  }
26
24
 
27
25
  void ObsInterface::list_encoders(obs_encoder_type type)
28
26
  {
29
- blog(LOG_INFO, "List encoders");
30
- blog(LOG_INFO, "List encoders of type: %d", type);
31
-
27
+ blog(LOG_INFO, "Encoders:");
32
28
  size_t idx = 0;
33
29
  const char *encoder_type;
34
30
 
35
31
  while (obs_enum_encoder_types(idx++, &encoder_type)) {
36
- if (obs_get_encoder_type(encoder_type) != type) {
37
- continue;
38
- }
39
-
40
- // if (obs_get_encoder_caps(encoder_type) & hide_flags) {
41
- // continue;
42
- // }
43
-
44
32
  blog(LOG_INFO, "\t- %s (%s)", encoder_type, obs_encoder_get_display_name(encoder_type));
45
33
  }
46
34
  };
47
35
 
48
36
  void ObsInterface::list_source_types()
49
37
  {
50
- blog(LOG_INFO, "List src types");
38
+ blog(LOG_INFO, "Sources:");
51
39
  size_t idx = 0;
52
40
  const char *src = nullptr;
53
41
 
@@ -56,20 +44,9 @@ void ObsInterface::list_source_types()
56
44
  }
57
45
  }
58
46
 
59
- void ObsInterface::list_input_types()
60
- {
61
- blog(LOG_INFO, "List input types");
62
- size_t idx = 0;
63
- const char *src = nullptr;
64
-
65
- while (obs_enum_input_types(idx++, &src)) {
66
- blog(LOG_INFO, "\t- %s", src);
67
- }
68
- }
69
-
70
47
  void ObsInterface::list_output_types()
71
48
  {
72
- blog(LOG_INFO, "List output types");
49
+ blog(LOG_INFO, "Outputs:");
73
50
  size_t idx = 0;
74
51
  const char *src = nullptr;
75
52
 
@@ -98,16 +75,50 @@ void ObsInterface::load_module(const char* module, const char* data) {
98
75
  }
99
76
  }
100
77
 
101
- void ObsInterface::reset_video() {
102
- blog(LOG_INFO, "Setup video info");
78
+ void ObsInterface::setVideoContext(int fps, int width, int height) {
79
+ blog(LOG_INFO, "Reset video context");
103
80
 
81
+ blog(LOG_INFO, "FPS: %d", fps);
82
+ blog(LOG_INFO, "Width: %d", width);
83
+ blog(LOG_INFO, "Height: %d", height);
84
+
85
+ if (fps <= 10) {
86
+ blog(LOG_WARNING, "Invalid FPS provided for reset, using default 10");
87
+ fps = 60;
88
+ }
89
+
90
+ if (width <= 32 || height <= 32) {
91
+ blog(LOG_WARNING, "Invalid width or height provided for reset, using default 1920x1080");
92
+ width = 1920;
93
+ height = 1080;
94
+ }
95
+
96
+ int ret = reset_video(fps, width, height);
97
+
98
+ if (ret == OBS_VIDEO_CURRENTLY_ACTIVE) {
99
+ blog(LOG_WARNING, "Can't reset video as currently active");
100
+ return;
101
+ }
102
+
103
+ if (ret != OBS_VIDEO_SUCCESS) {
104
+ blog(LOG_ERROR, "Failed to reset video context: %d", ret);
105
+ throw std::runtime_error("Failed to reset video context");
106
+ }
107
+
108
+ // Recreate the encoders as they are tied to the video context.
109
+ create_video_encoders();
110
+ }
111
+
112
+
113
+ int ObsInterface::reset_video(int fps, int width, int height) {
114
+ blog(LOG_INFO, "Reset video");
104
115
  obs_video_info ovi = {};
105
116
 
106
- ovi.base_width = 1920;
107
- ovi.base_height = 1080;
108
- ovi.output_width = 1920;
109
- ovi.output_height = 1080;
110
- ovi.fps_num = 60;
117
+ ovi.base_width = width;
118
+ ovi.base_height = height;
119
+ ovi.output_width = width;
120
+ ovi.output_height = height;
121
+ ovi.fps_num = fps;
111
122
  ovi.fps_den = 1;
112
123
 
113
124
  ovi.output_format = VIDEO_FORMAT_NV12;
@@ -118,29 +129,14 @@ void ObsInterface::reset_video() {
118
129
  ovi.gpu_conversion = true;
119
130
  ovi.graphics_module = "libobs-d3d11.dll";
120
131
 
121
- int success = obs_reset_video(&ovi);
122
-
123
- obs_enter_graphics();
124
- int dt = gs_get_device_type();
125
- blog(LOG_INFO, "Device type = %d", dt); // should be 1 for D3D11
126
- obs_leave_graphics();
127
-
128
- if (success != OBS_VIDEO_SUCCESS) {
129
- blog(LOG_ERROR, "Failed to reset video!");
130
- throw std::runtime_error("Failed to reset video!");
131
- }
132
+ return obs_reset_video(&ovi);
132
133
  }
133
134
 
134
- void ObsInterface::reset_audio() {
135
+ bool ObsInterface::reset_audio() {
135
136
  struct obs_audio_info oai = {0};
136
137
  oai.samples_per_sec = 48000;
137
138
  oai.speakers = SPEAKERS_STEREO;
138
- bool reset = obs_reset_audio(&oai);
139
-
140
- if (!reset) {
141
- blog(LOG_ERROR, "Failed to reset audio!");
142
- throw std::runtime_error("Failed to reset audio!");
143
- }
139
+ return obs_reset_audio(&oai);
144
140
  }
145
141
 
146
142
  void ObsInterface::init_obs(const std::string& distPath) {
@@ -166,10 +162,12 @@ void ObsInterface::init_obs(const std::string& distPath) {
166
162
 
167
163
  std::string effectsPath = basePath + "data/effects/";
168
164
  std::string pluginPath = basePath + "obs-plugins/";
165
+ std::string pluginDataPath = basePath + "data/obs-plugins/";
169
166
 
170
167
  blog(LOG_INFO, "Base path: %s", basePath.c_str());
171
168
  blog(LOG_INFO, "Effects path: %s", effectsPath.c_str());
172
169
  blog(LOG_INFO, "Plugin path: %s", pluginPath.c_str());
170
+ blog(LOG_INFO, "Data path: %s", pluginDataPath.c_str());
173
171
 
174
172
  // Add the effects path. We need this before resetting video and audio
175
173
  // to ensure the effects are available. The function is deprecated in
@@ -177,8 +175,18 @@ void ObsInterface::init_obs(const std::string& distPath) {
177
175
  obs_add_data_path(effectsPath.c_str());
178
176
 
179
177
  // This must come before loading modules to initialize D3D11.
180
- reset_video();
181
- reset_audio();
178
+ // Choose some sensible defaults that can be reconfigured.
179
+ int rc = reset_video(60, 1920, 1080);
180
+
181
+ if (rc != OBS_VIDEO_SUCCESS) {
182
+ blog(LOG_ERROR, "Failed to reset video!");
183
+ throw std::runtime_error("Failed to reset video!");
184
+ }
185
+
186
+ if (!reset_audio()) {
187
+ blog(LOG_ERROR, "Failed to reset audio!");
188
+ throw std::runtime_error("Failed to reset audio!");
189
+ }
182
190
 
183
191
  std::vector<std::string> modules = {
184
192
  "obs-x264",
@@ -190,7 +198,7 @@ void ObsInterface::init_obs(const std::string& distPath) {
190
198
 
191
199
  for (const auto& module : modules) {
192
200
  std::string modulePath = pluginPath + module + ".dll";
193
- std::string moduleDataPath = pluginPath + module;
201
+ std::string moduleDataPath = pluginDataPath + module;
194
202
  load_module(modulePath.c_str(), moduleDataPath.c_str());
195
203
  }
196
204
 
@@ -198,7 +206,6 @@ void ObsInterface::init_obs(const std::string& distPath) {
198
206
 
199
207
  list_encoders();
200
208
  list_source_types();
201
- list_input_types();
202
209
  list_output_types();
203
210
 
204
211
  blog(LOG_INFO, "Exit init_obs");
@@ -235,10 +242,10 @@ void ObsInterface::create_output() {
235
242
 
236
243
  blog(LOG_INFO, "Set ffmpeg_muxer settings");
237
244
  obs_data_t *ffmpeg_settings = obs_data_create();
238
- // Need to specify the exact path for ffmpeg_muxer.
245
+ // Need to specify the exact path for ffmpeg_muxer. We will write this again at start recording.
239
246
  std::string filename = recording_path + "\\" + get_current_date_time() + ".mp4";
240
247
  obs_data_set_string(ffmpeg_settings, "path", filename.c_str());
241
- recording_path = filename;
248
+ unbuffered_output_filename = filename;
242
249
 
243
250
  // Apply and release the settings.
244
251
  obs_output_update(file_output, ffmpeg_settings);
@@ -279,38 +286,48 @@ void ObsInterface::setRecordingDir(const std::string& recordingPath) {
279
286
  }
280
287
 
281
288
  void ObsInterface::create_video_encoders() {
282
- blog(LOG_INFO, "Create video encoder");
289
+ blog(LOG_INFO, "Set video encoder: %s", video_encoder_id.c_str());
283
290
 
284
- file_video_encoder = obs_video_encoder_create("obs_x264", "h264_stream_file", NULL, NULL);
291
+ if (file_video_encoder) {
292
+ blog(LOG_DEBUG, "Releasing file video encoder");
293
+ obs_encoder_release(file_video_encoder);
294
+ file_video_encoder = nullptr;
295
+ }
285
296
 
297
+ file_video_encoder = obs_video_encoder_create(
298
+ video_encoder_id.c_str(),
299
+ "noobs_file_encoder",
300
+ video_encoder_settings,
301
+ NULL
302
+ );
286
303
 
287
304
  if (!file_video_encoder) {
288
305
  blog(LOG_ERROR, "Failed to create video encoder!");
289
306
  throw std::runtime_error("Failed to create video encoder!");
290
307
  }
291
308
 
292
- buffer_video_encoder = obs_video_encoder_create("obs_x264", "h264_stream_buffer", NULL, NULL);
309
+ if (buffer_video_encoder) {
310
+ blog(LOG_DEBUG, "Releasing buffer video encoder");
311
+ obs_encoder_release(buffer_video_encoder);
312
+ buffer_video_encoder = nullptr;
313
+ }
314
+
315
+ buffer_video_encoder = obs_video_encoder_create(
316
+ video_encoder_id.c_str(),
317
+ "noobs_buffer_encoder",
318
+ video_encoder_settings,
319
+ NULL
320
+ );
293
321
 
294
322
  if (!buffer_video_encoder) {
295
323
  blog(LOG_ERROR, "Failed to create buffer video encoder!");
296
324
  throw std::runtime_error("Failed to create buffer video encoder!");
297
325
  }
298
326
 
299
- blog(LOG_INFO, "Set file video encoder settings");
300
- obs_data_t* venc_settings = obs_data_create();
301
- // obs_data_set_string(venc_settings, "preset", "speed"); // Faster preset
302
- obs_data_set_string(venc_settings, "rate_control", "CRF");
303
- obs_data_set_int(venc_settings, "crf", 22);
304
- obs_data_set_string(venc_settings, "profile", "main");
305
- obs_data_set_int(venc_settings, "keyint_sec", 1); // Set keyframe interval to 1 second
306
-
307
- obs_encoder_update(file_video_encoder, venc_settings);
308
- obs_encoder_update(buffer_video_encoder, venc_settings);
309
- obs_data_release(venc_settings);
310
-
311
327
  obs_output_set_video_encoder(file_output, file_video_encoder);
312
- obs_encoder_set_video(file_video_encoder, obs_get_video());
313
328
  obs_output_set_video_encoder(buffer_output, buffer_video_encoder);
329
+
330
+ obs_encoder_set_video(file_video_encoder, obs_get_video());
314
331
  obs_encoder_set_video(buffer_video_encoder, obs_get_video());
315
332
  }
316
333
 
@@ -375,12 +392,34 @@ void ObsInterface::create_scene() {
375
392
  obs_set_output_source(0, scene_source); // 0 = video track
376
393
  }
377
394
 
395
+ void ObsInterface::volmeter_callback(void *data,
396
+ const float magnitude[MAX_AUDIO_CHANNELS],
397
+ const float peak[MAX_AUDIO_CHANNELS],
398
+ const float inputPeak[MAX_AUDIO_CHANNELS])
399
+ {
400
+ // blog(LOG_DEBUG, "Volmeter callback triggered: %f %f %f",
401
+ // obs_db_to_mul(magnitude[0]),
402
+ // obs_db_to_mul(peak[0]),
403
+ // obs_db_to_mul(inputPeak[0])
404
+ // );
405
+
406
+ SignalContext* ctx = static_cast<SignalContext*>(data);
407
+ ObsInterface* self = ctx->self;
408
+
409
+ if (!self->volmeter_enabled) {
410
+ return;
411
+ }
412
+
413
+ SignalData* sd = new SignalData{ "volmeter", ctx->id.c_str(), 0, obs_db_to_mul(peak[0]) };
414
+ self->jscb.NonBlockingCall(sd, call_jscb);
415
+ }
416
+
378
417
  void ObsInterface::createSource(std::string name, std::string type) {
379
418
  blog(LOG_INFO, "Create source: %s of type %s", name.c_str(), type.c_str());
380
419
 
381
420
  obs_source_t *source = obs_source_create(
382
- type.c_str(),
383
- name.c_str(),
421
+ type.c_str(), // Type of source, e.g. "wasapi_input_capture"
422
+ name.c_str(), // Name of the source, e.g. "My Audio Input"
384
423
  NULL, // No settings.
385
424
  NULL // No hotkey data.
386
425
  );
@@ -390,6 +429,30 @@ void ObsInterface::createSource(std::string name, std::string type) {
390
429
  throw std::runtime_error("Failed to create source!");
391
430
  }
392
431
 
432
+ if (type == AUDIO_OUTPUT) {
433
+ blog(LOG_INFO, "Setting output volume for source: %s to %d", name.c_str(), output_volume);
434
+ obs_source_set_volume(source, output_volume);
435
+ } else if (type == AUDIO_INPUT) {
436
+ blog(LOG_INFO, "Setting input volume for source: %s to %d", name.c_str(), input_volume);
437
+ obs_source_set_volume(source, input_volume);
438
+ } else if (type == AUDIO_PROCESS) {
439
+ blog(LOG_INFO, "Setting process volume for source: %s to %d", name.c_str(), process_volume);
440
+ obs_source_set_volume(source, process_volume);
441
+ }
442
+
443
+ if (type == AUDIO_OUTPUT || type == AUDIO_INPUT || type == AUDIO_PROCESS) {
444
+ blog(LOG_INFO, "Creating volmeter for source: %s", name.c_str());
445
+
446
+ obs_volmeter_t *volmeter = obs_volmeter_create(OBS_FADER_CUBIC);
447
+ obs_volmeter_attach_source(volmeter, source);
448
+
449
+ SignalContext* ctx = new SignalContext{ this, name }; // TODO don't leak this.
450
+ obs_volmeter_add_callback(volmeter, volmeter_callback, ctx);
451
+
452
+ // Store the volmeter in the volmeters map.
453
+ volmeters[name] = volmeter;
454
+ }
455
+
393
456
  // Store the source in the sources map.
394
457
  sources[name] = source;
395
458
  }
@@ -397,6 +460,20 @@ void ObsInterface::createSource(std::string name, std::string type) {
397
460
  void ObsInterface::deleteSource(std::string name) {
398
461
  blog(LOG_INFO, "Delete source: %s", name.c_str());
399
462
 
463
+ // First release a volmeter if there is one present.
464
+ // Only audio sources have volmeters ofcourse.
465
+ auto vol_it = volmeters.find(name);
466
+
467
+ if (vol_it != volmeters.end()) {
468
+ obs_volmeter_t* volmeter = vol_it->second;
469
+ obs_volmeter_remove_callback(volmeter, volmeter_callback, this);
470
+ obs_volmeter_detach_source(volmeter);
471
+ obs_volmeter_destroy(volmeter);
472
+ blog(LOG_INFO, "Volmeter deleted for source: %s", name.c_str());
473
+ volmeters.erase(name);
474
+ }
475
+
476
+ // Now deal with the source itself.
400
477
  auto it = sources.find(name);
401
478
 
402
479
  if (it == sources.end()) {
@@ -405,6 +482,7 @@ void ObsInterface::deleteSource(std::string name) {
405
482
  }
406
483
 
407
484
  obs_source_t* source = it->second;
485
+ obs_source_remove(source); // ???
408
486
  obs_source_release(source);
409
487
  sources.erase(name);
410
488
  blog(LOG_INFO, "Source deleted: %s", name.c_str());
@@ -466,65 +544,30 @@ obs_properties_t* ObsInterface::getSourceProperties(std::string name) {
466
544
  return props;
467
545
  }
468
546
 
469
- void call_jscb(Napi::Env env, Napi::Function cb, SignalData* sd) {
470
- Napi::Object obj = Napi::Object::New(env);
471
- obj.Set("id", Napi::String::New(env, sd->id));
472
- obj.Set("code", Napi::Number::New(env, sd->code));
473
- cb.Call({ obj });
474
- delete sd;
475
- }
476
-
477
- void ObsInterface::output_signal_handler_starting(void *data, calldata_t *cd) {
478
- long long code = calldata_int(cd, "code");
479
- ObsInterface* self = static_cast<ObsInterface*>(data);
480
- SignalData* sd = new SignalData{ "starting", code };
481
- self->jscb.NonBlockingCall(sd, call_jscb);
482
- }
483
-
484
- void ObsInterface::output_signal_handler_start(void *data, calldata_t *cd) {
547
+ void ObsInterface::output_signal_handler(void *data, calldata_t *cd) {
485
548
  long long code = calldata_int(cd, "code");
486
- ObsInterface* self = static_cast<ObsInterface*>(data);
487
- SignalData* sd = new SignalData{ "start", code };
488
- self->jscb.NonBlockingCall(sd, call_jscb);
489
- }
490
-
491
- void ObsInterface::output_signal_handler_stop(void *data, calldata_t *cd) {
492
- long long code = calldata_int(cd, "code");
493
- ObsInterface* self = static_cast<ObsInterface*>(data);
494
- SignalData* sd = new SignalData{ "stop", code };
495
- self->jscb.NonBlockingCall(sd, call_jscb);
496
- }
497
549
 
498
- void ObsInterface::output_signal_handler_stopping(void *data, calldata_t *cd) {
499
- long long code = calldata_int(cd, "code");
500
- ObsInterface* self = static_cast<ObsInterface*>(data);
501
- SignalData* sd = new SignalData{ "stopping", code };
502
- self->jscb.NonBlockingCall(sd, call_jscb);
503
- }
550
+ SignalContext* ctx = static_cast<SignalContext*>(data);
551
+ ObsInterface* self = ctx->self;
504
552
 
505
- void ObsInterface::output_signal_handler_saved(void *data, calldata_t *cd) {
506
- long long code = calldata_int(cd, "code");
507
- ObsInterface* self = static_cast<ObsInterface*>(data);
508
- SignalData* sd = new SignalData{ "saved", code };
553
+ SignalData* sd = new SignalData{ "output", ctx->id.c_str(), code };
509
554
  self->jscb.NonBlockingCall(sd, call_jscb);
510
555
  }
511
556
 
512
557
  void ObsInterface::connect_signal_handlers(obs_output_t *output) {
513
558
  signal_handler_t *sh = obs_output_get_signal_handler(output);
514
- signal_handler_connect(sh, "starting", output_signal_handler_starting, this);
515
- signal_handler_connect(sh, "start", output_signal_handler_start, this);
516
- signal_handler_connect(sh, "stopping", output_signal_handler_stopping, this);
517
- signal_handler_connect(sh, "stop", output_signal_handler_stop, this);
518
- signal_handler_connect(sh, "saved", output_signal_handler_saved, this);
559
+ signal_handler_connect(sh, "start", output_signal_handler, start_ctx);
560
+ signal_handler_connect(sh, "starting", output_signal_handler, starting_ctx);
561
+ signal_handler_connect(sh, "stopping", output_signal_handler, stopping_ctx);
562
+ signal_handler_connect(sh, "stop", output_signal_handler, stop_ctx);
519
563
  }
520
564
 
521
565
  void ObsInterface::disconnect_signal_handlers(obs_output_t *output) {
522
566
  signal_handler_t *sh = obs_output_get_signal_handler(output);
523
- signal_handler_disconnect(sh, "starting", output_signal_handler_starting, this);
524
- signal_handler_disconnect(sh, "start", output_signal_handler_start, this);
525
- signal_handler_disconnect(sh, "stopping", output_signal_handler_stopping, this);
526
- signal_handler_disconnect(sh, "stop", output_signal_handler_stop, this);
527
- signal_handler_disconnect(sh, "saved", output_signal_handler_saved, this);
567
+ signal_handler_disconnect(sh, "starting", output_signal_handler, starting_ctx);
568
+ signal_handler_disconnect(sh, "start", output_signal_handler, start_ctx);
569
+ signal_handler_disconnect(sh, "stopping", output_signal_handler, stopping_ctx);
570
+ signal_handler_disconnect(sh, "stop", output_signal_handler, stop_ctx);
528
571
  }
529
572
 
530
573
  bool draw_source_outline(obs_scene_t *scene, obs_sceneitem_t *item, void *p) {
@@ -582,6 +625,12 @@ bool draw_source_outline(obs_scene_t *scene, obs_sceneitem_t *item, void *p) {
582
625
  gs_draw_sprite(nullptr, 0, 4.0f, height);
583
626
  gs_matrix_pop();
584
627
 
628
+ // Dragging point box (25x25 pixels in bottom-right corner)
629
+ gs_matrix_push();
630
+ gs_matrix_translate3f(pos.x + width - 25.0f, pos.y + height - 25.0f, 0.0f);
631
+ gs_draw_sprite(nullptr, 0, 25.0f, 25.0f);
632
+ gs_matrix_pop();
633
+
585
634
  gs_matrix_pop();
586
635
 
587
636
  gs_technique_end_pass(tech);
@@ -683,7 +732,7 @@ void ObsInterface::initPreview(HWND parent) {
683
732
  obs_display_set_enabled(display, false);
684
733
  }
685
734
 
686
- void ObsInterface::showPreview(int x, int y, int width, int height) {
735
+ void ObsInterface::configurePreview(int x, int y, int width, int height) {
687
736
  blog(LOG_INFO, "ObsInterface::showPreview");
688
737
 
689
738
  if (!preview_hwnd || !display) {
@@ -691,7 +740,7 @@ void ObsInterface::showPreview(int x, int y, int width, int height) {
691
740
  return;
692
741
  }
693
742
 
694
- blog(LOG_INFO, "Showing preview child window at (%d, %d) with size (%d x %d)", x, y, width, height);
743
+ blog(LOG_INFO, "Moving preview child window to (%d, %d) with size (%d x %d)", x, y, width, height);
695
744
 
696
745
  // Resize and move the existing child window.
697
746
  bool success = SetWindowPos(
@@ -710,8 +759,18 @@ void ObsInterface::showPreview(int x, int y, int width, int height) {
710
759
  uint32_t w, h;
711
760
  obs_display_size(display, &w, &h); // Get the display size to match the video context.
712
761
  blog(LOG_INFO, "Current Display size set to (%d x %d)", w, h);
713
-
714
762
  obs_display_resize(display, width, height);
763
+ obs_display_set_enabled(display, true);
764
+ }
765
+
766
+ void ObsInterface::showPreview() {
767
+ blog(LOG_INFO, "ObsInterface::showPreview");
768
+
769
+ if (!preview_hwnd || !display) {
770
+ blog(LOG_ERROR, "Preview window not initialized");
771
+ return;
772
+ }
773
+
715
774
  ShowWindow(preview_hwnd, SW_SHOW);
716
775
  obs_display_set_enabled(display, true);
717
776
  }
@@ -723,14 +782,24 @@ void ObsInterface::hidePreview() {
723
782
  ShowWindow(preview_hwnd, SW_HIDE);
724
783
  blog(LOG_INFO, "Preview child window hidden");
725
784
  }
785
+ }
786
+
787
+ void ObsInterface::disablePreview() {
788
+ blog(LOG_INFO, "ObsInterface::disablePreview");
726
789
 
790
+ if (!display) {
791
+ blog(LOG_ERROR, "Preview window not initialized");
792
+ return;
793
+ }
794
+
795
+ hidePreview();
727
796
  obs_display_set_enabled(display, false);
728
797
  }
729
798
 
730
- float ObsInterface::getPreviewScaleFactor() {
799
+ PreviewInfo ObsInterface::getPreviewInfo() {
731
800
  if (!display) {
732
- blog(LOG_WARNING, "Display not initialized");
733
- return 1.0f; // Default scale
801
+ blog(LOG_WARNING, "Display not initialized when calling getPreviewInfo");
802
+ return { 1920, 1080, 1920, 1080 }; // Default values
734
803
  }
735
804
 
736
805
  obs_video_info ovi;
@@ -739,19 +808,14 @@ float ObsInterface::getPreviewScaleFactor() {
739
808
  uint32_t width, height;
740
809
  obs_display_size(display, &width, &height);
741
810
 
742
- float scaleX = float(width) / float(ovi.base_width);
743
- float scaleY = float(height) / float(ovi.base_height);
744
-
745
- float previewScale;
746
-
747
- // Pick the limiting scale factor.
748
- if (scaleX < scaleY) {
749
- previewScale = scaleX;
750
- } else {
751
- previewScale = scaleY;
752
- }
811
+ PreviewInfo info = {
812
+ ovi.base_width,
813
+ ovi.base_height,
814
+ width,
815
+ height,
816
+ };
753
817
 
754
- return previewScale;
818
+ return info;
755
819
  }
756
820
 
757
821
  void ObsInterface::setDrawSourceOutline(bool enabled) {
@@ -779,10 +843,17 @@ ObsInterface::ObsInterface(
779
843
  jscb = cb;
780
844
  recording_path = recordingPath;
781
845
 
846
+ starting_ctx = new SignalContext{ this, "starting" };
847
+ start_ctx = new SignalContext{ this, "start" };
848
+ stopping_ctx = new SignalContext{ this, "stopping" };
849
+ stop_ctx = new SignalContext{ this, "stop" };
850
+
782
851
  // Create the resources we rely on.
783
852
  create_output();
784
853
  create_scene();
785
854
 
855
+ video_encoder_id = "obs_x264";
856
+ video_encoder_settings = obs_data_create();
786
857
  create_video_encoders();
787
858
  create_audio_encoders();
788
859
  }
@@ -790,16 +861,26 @@ ObsInterface::ObsInterface(
790
861
  ObsInterface::~ObsInterface() {
791
862
  blog(LOG_DEBUG, "Destroying ObsInterface");
792
863
 
793
- if (jscb) {
794
- blog(LOG_DEBUG, "Releasing JavaScript callback");
795
- jscb.Release();
864
+ for (auto& kv : volmeters) {
865
+ obs_volmeter_t* volmeter = kv.second;
866
+ obs_volmeter_remove_callback(volmeter, volmeter_callback, this);
867
+ obs_volmeter_detach_source(volmeter);
868
+ obs_volmeter_destroy(volmeter);
869
+ blog(LOG_INFO, "Volmeter deleted for source: %s", kv.first.c_str());
870
+ volmeters.erase(kv.first);
796
871
  }
797
872
 
873
+ delete starting_ctx;
874
+ delete start_ctx;
875
+ delete stopping_ctx;
876
+ delete stop_ctx;
877
+
798
878
  for (auto& kv : sources) {
799
879
  std::string name = kv.first;
800
880
  obs_source_t* source = kv.second;
801
881
  blog(LOG_DEBUG, "Releasing source: %s", name.c_str());
802
882
  obs_source_release(source);
883
+ sources.erase(name);
803
884
  }
804
885
 
805
886
  if (scene) {
@@ -839,6 +920,11 @@ ObsInterface::~ObsInterface() {
839
920
 
840
921
  blog(LOG_DEBUG, "Now shutting down OBS");
841
922
  obs_shutdown();
923
+
924
+ if (jscb) {
925
+ blog(LOG_DEBUG, "Releasing JavaScript callback");
926
+ jscb.Release();
927
+ }
842
928
  }
843
929
 
844
930
  bool ObsInterface::setBuffering(bool value) {
@@ -909,6 +995,14 @@ void ObsInterface::startRecording(int offset) {
909
995
  throw std::runtime_error("Failed to call convert procedure handler");
910
996
  }
911
997
  } else {
998
+
999
+ obs_data_t *ffmpeg_settings = obs_data_create();
1000
+ std::string filename = recording_path + "\\" + get_current_date_time() + ".mp4";
1001
+ obs_data_set_string(ffmpeg_settings, "path", filename.c_str());
1002
+ obs_output_update(output, ffmpeg_settings);
1003
+ obs_data_release(ffmpeg_settings);
1004
+ unbuffered_output_filename = filename;
1005
+
912
1006
  blog(LOG_INFO, "Starting ffmpeg_muxer output");
913
1007
 
914
1008
  bool is_active = obs_output_active(output);
@@ -945,6 +1039,20 @@ void ObsInterface::stopRecording() {
945
1039
  blog(LOG_INFO, "ObsInterface::stopRecording exited");
946
1040
  }
947
1041
 
1042
+ void ObsInterface::forceStopRecording() {
1043
+ blog(LOG_INFO, "ObsInterface::forceStopRecording enter");
1044
+ obs_output_t* output = buffering ? buffer_output : file_output;
1045
+ bool is_active = obs_output_active(output);
1046
+
1047
+ if (!is_active) {
1048
+ blog(LOG_WARNING, "Output is not active");
1049
+ return;
1050
+ }
1051
+
1052
+ obs_output_force_stop(output);
1053
+ blog(LOG_INFO, "ObsInterface::forceStopRecording exited");
1054
+ }
1055
+
948
1056
  std::string ObsInterface::getLastRecording() {
949
1057
  blog(LOG_INFO, "calling get last replay proc handler");
950
1058
  calldata cd;
@@ -957,7 +1065,7 @@ std::string ObsInterface::getLastRecording() {
957
1065
 
958
1066
  if (!buffering) {
959
1067
  blog(LOG_INFO, "Getting last recording path from ffmpeg_muxer");
960
- return recording_path;
1068
+ return unbuffered_output_filename;
961
1069
  }
962
1070
 
963
1071
  bool success = proc_handler_call(ph, "get_last_replay", &cd);
@@ -1057,4 +1165,114 @@ void ObsInterface::setSourcePos(std::string name, vec2* pos, vec2* scale) {
1057
1165
 
1058
1166
  obs_sceneitem_set_pos(item, pos);
1059
1167
  obs_sceneitem_set_scale(item, scale);
1168
+ }
1169
+
1170
+ std::vector<std::string> ObsInterface::listAvailableVideoEncoders()
1171
+ {
1172
+ std::vector<std::string> encoders;
1173
+ size_t idx = 0;
1174
+ const char *encoder_type;
1175
+
1176
+ while (obs_enum_encoder_types(idx++, &encoder_type)) {
1177
+ bool video = obs_get_encoder_type(encoder_type) == OBS_ENCODER_VIDEO;
1178
+
1179
+ if (video)
1180
+ encoders.emplace_back(encoder_type);
1181
+ }
1182
+
1183
+ return encoders;
1184
+ }
1185
+
1186
+ void ObsInterface::setVideoEncoder(std::string id, obs_data_t* settings) {
1187
+ // TODO don't allow this if output is active.
1188
+ video_encoder_id = id;
1189
+ video_encoder_settings = settings;
1190
+ create_video_encoders();
1191
+ }
1192
+
1193
+ void ObsInterface::setMuteAudioInputs(bool mute) {
1194
+ // Loop over all sources, and set the mute state if they are of type "wasapi_input_capture".
1195
+ for (const auto& kv : sources) {
1196
+ const std::string& name = kv.first;
1197
+ obs_source_t* source = kv.second;
1198
+
1199
+ if (!source) {
1200
+ blog(LOG_WARNING, "Source %s not found", name.c_str());
1201
+ continue;
1202
+ }
1203
+
1204
+ const char* type = obs_source_get_id(source);
1205
+
1206
+ if (strcmp(type, AUDIO_INPUT) == 0) {
1207
+ obs_source_set_muted(source, mute);
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ void ObsInterface::setOutputVolume(float volume) {
1213
+ blog(LOG_INFO, "Setting output volume to %f", volume);
1214
+ output_volume = volume;
1215
+
1216
+ for (const auto& kv : sources) {
1217
+ const std::string& name = kv.first;
1218
+ obs_source_t* source = kv.second;
1219
+
1220
+ if (!source) {
1221
+ blog(LOG_WARNING, "Source %s not found", name.c_str());
1222
+ continue;
1223
+ }
1224
+
1225
+ const char* type = obs_source_get_id(source);
1226
+
1227
+ if (strcmp(type, AUDIO_OUTPUT) == 0) {
1228
+ obs_source_set_volume(source, output_volume);
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+ void ObsInterface::setInputVolume(float volume) {
1234
+ blog(LOG_INFO, "Setting input volume to %f", volume);
1235
+ input_volume = volume;
1236
+
1237
+ for (const auto& kv : sources) {
1238
+ const std::string& name = kv.first;
1239
+ obs_source_t* source = kv.second;
1240
+
1241
+ if (!source) {
1242
+ blog(LOG_WARNING, "Source %s not found", name.c_str());
1243
+ continue;
1244
+ }
1245
+
1246
+ const char* type = obs_source_get_id(source);
1247
+
1248
+ if (strcmp(type, AUDIO_INPUT) == 0) {
1249
+ obs_source_set_volume(source, input_volume);
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ void ObsInterface::setProcessVolume(float volume) {
1255
+ blog(LOG_INFO, "Setting process volume to %f", volume);
1256
+ process_volume = volume;
1257
+
1258
+ for (const auto& kv : sources) {
1259
+ const std::string& name = kv.first;
1260
+ obs_source_t* source = kv.second;
1261
+
1262
+ if (!source) {
1263
+ blog(LOG_WARNING, "Source %s not found", name.c_str());
1264
+ continue;
1265
+ }
1266
+
1267
+ const char* type = obs_source_get_id(source);
1268
+
1269
+ if (strcmp(type, AUDIO_PROCESS) == 0) {
1270
+ obs_source_set_volume(source, process_volume);
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ void ObsInterface::setVolmeterEnabled(bool enabled) {
1276
+ blog(LOG_INFO, "Setting volmeter enabled: %d", enabled);
1277
+ volmeter_enabled = enabled;
1060
1278
  }
@@ -4,10 +4,30 @@
4
4
  #include <napi.h>
5
5
  #include <windows.h>
6
6
  #include <map>
7
+ #include <string>
8
+ #include <optional>
9
+
10
+ #define AUDIO_INPUT "wasapi_input_capture"
11
+ #define AUDIO_OUTPUT "wasapi_output_capture"
12
+ #define AUDIO_PROCESS "wasapi_process_output_capture"
13
+
14
+ class ObsInterface;
7
15
 
8
16
  struct SignalData {
17
+ std::string type;
9
18
  std::string id;
10
19
  long long code;
20
+ std::optional<float> value;
21
+ };
22
+
23
+ struct SignalContext {
24
+ ObsInterface* self;
25
+ std::string id;
26
+ };
27
+
28
+ struct PreviewInfo {
29
+ uint32_t canvasWidth, canvasHeight;
30
+ uint32_t displayWidth, displayHeight;
11
31
  };
12
32
 
13
33
  class ObsInterface {
@@ -22,17 +42,25 @@ class ObsInterface {
22
42
  ~ObsInterface();
23
43
 
24
44
  bool setBuffering(bool buffering); // In buffering mode, the recording is stored in memory and can be converted to a file later.
25
- void startBuffering(); // Start buffering to memory.
26
- void startRecording(int offset); // Convert the active buffered recording to a real one.
27
- void stopRecording(); // Stop the recording.
28
- std::string getLastRecording(); // Get the last recorded file path.
45
+ void startBuffering(); // Start buffering to memory.
46
+ void startRecording(int offset); // Convert the active buffered recording to a real one.
47
+ void stopRecording(); // Stop the recording.
48
+ void forceStopRecording(); // Force stop the recording, this will not save the current recording.
49
+ std::string getLastRecording(); // Get the last recorded file path.
29
50
  void setRecordingDir(const std::string& recordingPath); // Output must not be active when calling this.
51
+ void setVideoContext(int fps, int width, int height); // Reset video settings.
30
52
 
31
- void createSource(std::string name, std::string type); // Create a new source
32
- void deleteSource(std::string name); // Release a source.
33
- obs_data_t* getSourceSettings(std::string name); // Get the current settings.
53
+ void createSource(std::string name, std::string type); // Create a new source
54
+ void deleteSource(std::string name); // Release a source.
55
+ obs_data_t* getSourceSettings(std::string name); // Get the current settings.
34
56
  void setSourceSettings(std::string name, obs_data_t* settings); // Set settings.
35
- obs_properties_t* getSourceProperties(std::string name); // Get the settings schema.
57
+ obs_properties_t* getSourceProperties(std::string name); // Get the settings schema.
58
+ void setMuteAudioInputs(bool mute); // Mute or unmute all audio inputs.
59
+ void setOutputVolume(float volume);
60
+ void setInputVolume(float volume);
61
+ void setProcessVolume(float volume);
62
+
63
+ void setVolmeterEnabled(bool enabled);
36
64
 
37
65
  void addSourceToScene(std::string name); // Add source to scene.
38
66
  void removeSourceFromScene(std::string name); // Remove source from scene.
@@ -40,49 +68,56 @@ class ObsInterface {
40
68
  void setSourcePos(std::string name, vec2* pos, vec2* scale); // Size does not get set here because it's set by the source itself.
41
69
 
42
70
  void initPreview(HWND parent); // Must call this before showPreview to setup resources.
43
- void showPreview(int x, int y, int width, int height); // Also used for moving and resizing.
44
- void hidePreview(); // Hide the preview display.
45
- float getPreviewScaleFactor(); // Get the current scale factor of the preview.
71
+ void configurePreview(int x, int y, int width, int height); // Move and resize the preview display.
72
+ void showPreview(); // Show the preview display.
73
+ void hidePreview(); // Hide the preview display, but leave it running.
74
+ void disablePreview(); // Disable the preview display, to save resources.
75
+ PreviewInfo getPreviewInfo(); // Get the dimensions of the display, and the base canvas.
46
76
  void setDrawSourceOutline(bool enabled); // Red box around source
47
77
  bool getDrawSourceOutlineEnabled();
48
78
 
49
- std::vector<std::string> get_available_video_encoders();
50
-
51
- // TODO
52
- // Configure video
53
- // Configure audio
54
- // List audio source
55
- // Reconfigure audio sources
56
- // Reconfigure video sources
79
+ std::vector<std::string> listAvailableVideoEncoders(); // Return a list of available video encoders.
80
+ void setVideoEncoder(std::string id, obs_data_t* settings); // Set the video encoder to use.
57
81
 
58
82
  private:
59
83
  obs_output_t *file_output = nullptr;
60
84
  obs_output_t *buffer_output = nullptr;
85
+
61
86
  obs_scene_t *scene = nullptr;
87
+
62
88
  obs_encoder_t *file_video_encoder = nullptr;
63
89
  obs_encoder_t *file_audio_encoder = nullptr;
90
+
64
91
  obs_encoder_t *buffer_video_encoder = nullptr;
65
92
  obs_encoder_t *buffer_audio_encoder = nullptr;
93
+
94
+ float output_volume = 1.0f; // Volume for the output.
95
+ float input_volume = 1.0f; // Volume for the input.
96
+ float process_volume = 1.0f; // Volume for the process.
97
+
66
98
  obs_display_t *display = nullptr;
67
99
  HWND preview_hwnd = nullptr; // window handle for scene preview
68
100
  Napi::ThreadSafeFunction jscb; // javascript callback
69
101
  std::string recording_path = "";
102
+ std::string unbuffered_output_filename = "";
103
+
70
104
  bool buffering = false; // Whether we are buffering the recording in memory.
71
105
  bool drawSourceOutline = false; // Draw red outline around source
72
106
  std::map<std::string, obs_source_t*> sources; // Map of source names to obs_source_t pointers.
107
+ std::map<std::string, obs_volmeter_t*> volmeters; // Map of source names to obs_volmeter_t pointers.
73
108
 
74
109
  void init_obs(const std::string& distPath);
75
- void reset_video();
76
- void reset_audio();
110
+ int reset_video(int fps, int width, int height);
111
+ bool reset_audio();
77
112
  void load_module(const char* module, const char* data); // Load a module, data is optional.
78
113
  void connect_signal_handlers(obs_output_t *output);
79
114
  void disconnect_signal_handlers(obs_output_t *output);
80
115
 
81
- static void output_signal_handler_starting(void *data, calldata_t *cd);
82
- static void output_signal_handler_start(void *data, calldata_t *cd);
83
- static void output_signal_handler_stop(void *data, calldata_t *cd);
84
- static void output_signal_handler_stopping(void *data, calldata_t *cd);
85
- static void output_signal_handler_saved(void *data, calldata_t *cd);
116
+ SignalContext* starting_ctx;
117
+ SignalContext* start_ctx;
118
+ SignalContext* stopping_ctx;
119
+ SignalContext* stop_ctx;
120
+ static void output_signal_handler(void *data, calldata_t *cd);
86
121
 
87
122
  void list_encoders(obs_encoder_type type = OBS_ENCODER_VIDEO);
88
123
  void list_source_types();
@@ -92,6 +127,17 @@ class ObsInterface {
92
127
  void create_scene();
93
128
  void create_output();
94
129
 
130
+ std::string video_encoder_id; // The video encoder ID to use.
131
+ obs_data_t* video_encoder_settings; // Settings for the video encoder.
95
132
  void create_video_encoders();
96
133
  void create_audio_encoders();
134
+
135
+ bool volmeter_enabled = false; // Whether the volmeter callback is enabled.
136
+
137
+ static void volmeter_callback(
138
+ void *data,
139
+ const float magnitude[MAX_AUDIO_CHANNELS],
140
+ const float peak[MAX_AUDIO_CHANNELS],
141
+ const float inputPeak[MAX_AUDIO_CHANNELS]
142
+ );
97
143
  };