noobs 0.0.60 → 0.0.64
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/README.md +45 -1
- package/dist/noobs.node +0 -0
- package/index.d.ts +20 -14
- package/index.js +3 -3
- package/package.json +1 -1
- package/src/main.cpp +31 -6
- package/src/obs_interface.cpp +187 -149
- package/src/obs_interface.h +13 -8
- package/src/utils.cpp +8 -0
- package/src/utils.h +2 -1
package/README.md
CHANGED
|
@@ -19,12 +19,56 @@ npm install noobs
|
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
|
+
### Lifecycle
|
|
22
23
|
```javascript
|
|
23
24
|
import noobs from 'noobs';
|
|
24
25
|
|
|
26
|
+
const pluginPath = ...;
|
|
27
|
+
const logPath = ...;
|
|
28
|
+
const dataPath = ...;
|
|
29
|
+
const recordingPath = ...;
|
|
30
|
+
const cb = (signal) => console.log(signal);
|
|
31
|
+
|
|
32
|
+
noobs.Init(pluginPath, logPath, dataPath, recordingPath, cb);
|
|
33
|
+
...
|
|
34
|
+
noobs.Shutdown();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Sources
|
|
38
|
+
```javascript
|
|
39
|
+
noobs.CreateSource('Test Source', 'monitor_capture'); // Creates a source
|
|
40
|
+
noobs.AddSourceToScene('Test Source'); // Add the source to the scene
|
|
41
|
+
const s = noobs.GetSourceSettings('Test Source'); // View current settings
|
|
42
|
+
const p = noobs.GetSourceProperties('Test Source'); // View settings schema
|
|
43
|
+
noobs.SetSourceSettings('Test Source', { ...s, monitor: 1 });
|
|
44
|
+
noobs.RemoveSourceFromScene('Test Source'); // Remove a source from the scene
|
|
45
|
+
noobs.DeleteSource('Test Source') // Release a source
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Basic Recording Usage
|
|
49
|
+
```javascript
|
|
50
|
+
noobs.StartRecording();
|
|
25
51
|
...
|
|
52
|
+
noobs.StopRecording();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Buffer Recording
|
|
56
|
+
```javascript
|
|
57
|
+
noobs.SetBuffering(true);
|
|
58
|
+
|
|
59
|
+
noobs.StartBuffering();
|
|
60
|
+
...
|
|
61
|
+
noobs.StartRecording(5); // include last 5 seconds of the recording buffer
|
|
62
|
+
...
|
|
63
|
+
noobs.StopRecording();
|
|
64
|
+
```
|
|
26
65
|
|
|
27
|
-
|
|
66
|
+
### Preview
|
|
67
|
+
```javascript
|
|
68
|
+
const hwnd = this.mainWindow.getNativeWindowHandle();
|
|
69
|
+
noobs.InitPreview(hwnd);
|
|
70
|
+
noobs.ShowPreview(x, y, width, height); // Use this for moving/resizing
|
|
71
|
+
// noobs.HidePreview();
|
|
28
72
|
```
|
|
29
73
|
|
|
30
74
|
See `test.js` for more.
|
package/dist/noobs.node
CHANGED
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// OBS Data Types
|
|
2
|
-
export type ObsDataValue =
|
|
2
|
+
export type ObsDataValue =
|
|
3
|
+
| string
|
|
4
|
+
| number
|
|
5
|
+
| boolean
|
|
6
|
+
| ObsData
|
|
7
|
+
| ObsData[]
|
|
8
|
+
| null;
|
|
3
9
|
|
|
4
10
|
export interface ObsData {
|
|
5
11
|
[key: string]: ObsDataValue;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
// OBS Property Types
|
|
9
|
-
export type ObsPropertyType =
|
|
15
|
+
export type ObsPropertyType =
|
|
10
16
|
| 'invalid'
|
|
11
17
|
| 'bool'
|
|
12
|
-
| 'int'
|
|
18
|
+
| 'int'
|
|
13
19
|
| 'float'
|
|
14
20
|
| 'text'
|
|
15
21
|
| 'path'
|
|
@@ -114,7 +120,7 @@ export interface ObsGenericProperty extends ObsPropertyBase {
|
|
|
114
120
|
type: 'invalid' | 'unknown';
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
export type ObsProperty =
|
|
123
|
+
export type ObsProperty =
|
|
118
124
|
| ObsIntProperty
|
|
119
125
|
| ObsFloatProperty
|
|
120
126
|
| ObsTextProperty
|
|
@@ -130,35 +136,35 @@ export type ObsProperty =
|
|
|
130
136
|
| ObsGenericProperty;
|
|
131
137
|
|
|
132
138
|
export type Signal = {
|
|
133
|
-
id: string;
|
|
139
|
+
id: string; // Signal identifier, e.g. "stop"
|
|
134
140
|
code: number; // 0 for success, other values for errors
|
|
135
|
-
}
|
|
141
|
+
};
|
|
136
142
|
|
|
137
143
|
export type SceneItemPosition = {
|
|
138
|
-
x: number;
|
|
139
|
-
y: number;
|
|
144
|
+
x: number; // X position in pixels
|
|
145
|
+
y: number; // Y position in pixels
|
|
140
146
|
scaleX: number; // X scaling factor
|
|
141
147
|
scaleY: number; // Y scaling factor
|
|
142
148
|
};
|
|
143
149
|
|
|
144
150
|
export type SourceDimensions = {
|
|
145
151
|
height: number; // Height in pixels, before scaling
|
|
146
|
-
width: number;
|
|
147
|
-
}
|
|
152
|
+
width: number; // Width in pixels, before scaling
|
|
153
|
+
};
|
|
148
154
|
|
|
149
155
|
interface Noobs {
|
|
150
156
|
Init(
|
|
151
|
-
pluginPath: string,
|
|
152
|
-
logPath: string,
|
|
157
|
+
pluginPath: string,
|
|
158
|
+
logPath: string,
|
|
153
159
|
dataPath: string,
|
|
154
160
|
recordingPath: string,
|
|
155
161
|
cb: (signal: Signal) => void,
|
|
156
|
-
buffering: boolean,
|
|
157
162
|
): void;
|
|
158
163
|
|
|
159
164
|
Shutdown(): void;
|
|
160
|
-
|
|
165
|
+
|
|
161
166
|
// Recording functions
|
|
167
|
+
SetBuffering(buffering: boolean): void; // In buffering mode, the recording is stored in memory and can be converted to a file later.
|
|
162
168
|
StartBuffer(): void;
|
|
163
169
|
StartRecording(offset: number): void;
|
|
164
170
|
StopRecording(): void;
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
|
-
process.env.Path +=
|
|
4
|
-
process.env.Path += path.resolve(__dirname, 'dist', 'bin');
|
|
3
|
+
process.env.Path += ';';
|
|
4
|
+
process.env.Path += path.resolve(__dirname, 'dist', 'bin').replace('app.asar', 'app.asar.unpacked');
|
|
5
5
|
|
|
6
6
|
const packageName = 'noobs.node';
|
|
7
|
-
const noobs = require(`./dist/${packageName}`)
|
|
7
|
+
const noobs = require(`./dist/${packageName}`);
|
|
8
8
|
module.exports = noobs;
|
package/package.json
CHANGED
package/src/main.cpp
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
ObsInterface* obs = nullptr;
|
|
8
8
|
|
|
9
9
|
Napi::Value ObsInit(const Napi::CallbackInfo& info) {
|
|
10
|
-
bool valid = info.Length() ==
|
|
10
|
+
bool valid = info.Length() == 5 &&
|
|
11
11
|
info[0].IsString() && // Plugin path
|
|
12
12
|
info[1].IsString() && // Log path
|
|
13
13
|
info[2].IsString() && // Data path
|
|
14
14
|
info[3].IsString() && // Recording path
|
|
15
|
-
info[4].IsFunction()
|
|
16
|
-
info[5].IsBoolean(); // IsBuffer
|
|
15
|
+
info[4].IsFunction(); // JavaScript callback
|
|
17
16
|
|
|
18
17
|
if (!valid) {
|
|
19
18
|
Napi::Error::New(info.Env(), "Invalid arguments passed to ObsInit").ThrowAsJavaScriptException();
|
|
@@ -25,12 +24,11 @@ Napi::Value ObsInit(const Napi::CallbackInfo& info) {
|
|
|
25
24
|
std::string dataPath = info[2].As<Napi::String>().Utf8Value();
|
|
26
25
|
std::string recordingPath = info[3].As<Napi::String>().Utf8Value();
|
|
27
26
|
Napi::Function fn = info[4].As<Napi::Function>();
|
|
28
|
-
bool isBuffer = info[5].As<Napi::Boolean>().Value();
|
|
29
27
|
|
|
30
28
|
Napi::ThreadSafeFunction jscb =
|
|
31
29
|
Napi::ThreadSafeFunction::New(info.Env(), fn, "JavaScript callback", 0, 1);
|
|
32
30
|
|
|
33
|
-
obs = new ObsInterface(pluginPath, logPath, dataPath, recordingPath, jscb
|
|
31
|
+
obs = new ObsInterface(pluginPath, logPath, dataPath, recordingPath, jscb);
|
|
34
32
|
return info.Env().Undefined();
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -54,7 +52,33 @@ Napi::Value ObsSetRecordingDir(const Napi::CallbackInfo& info) {
|
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
std::string recordingPath = info[0].As<Napi::String>().Utf8Value();
|
|
57
|
-
obs->
|
|
55
|
+
obs->setRecordingDir(recordingPath);
|
|
56
|
+
return info.Env().Undefined();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Napi::Value ObsSetBuffering(const Napi::CallbackInfo& info) {
|
|
60
|
+
blog(LOG_INFO, "ObsSetBuffering called");
|
|
61
|
+
|
|
62
|
+
if (!obs) {
|
|
63
|
+
blog(LOG_ERROR, "ObsSetBuffering called but obs is not initialized");
|
|
64
|
+
throw std::runtime_error("Obs not initialized");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool valid = info.Length() == 1 && info[0].IsBoolean();
|
|
68
|
+
|
|
69
|
+
if (!valid) {
|
|
70
|
+
Napi::TypeError::New(info.Env(), "Invalid arguments passed to ObsSetBuffering").ThrowAsJavaScriptException();
|
|
71
|
+
return info.Env().Undefined();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bool buffering = info[0].As<Napi::Boolean>().Value();
|
|
75
|
+
bool success = obs->setBuffering(buffering);
|
|
76
|
+
|
|
77
|
+
if (!success) {
|
|
78
|
+
Napi::Error::New(info.Env(), "Failed to set buffering mode, is recording active?").ThrowAsJavaScriptException();
|
|
79
|
+
return info.Env().Undefined();
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
return info.Env().Undefined();
|
|
59
83
|
}
|
|
60
84
|
|
|
@@ -399,6 +423,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
399
423
|
exports.Set("Shutdown", Napi::Function::New(env, ObsShutdown));
|
|
400
424
|
exports.Set("SetRecordingDir", Napi::Function::New(env, ObsSetRecordingDir));
|
|
401
425
|
|
|
426
|
+
exports.Set("SetBuffering", Napi::Function::New(env, ObsSetBuffering));
|
|
402
427
|
exports.Set("StartBuffer", Napi::Function::New(env, ObsStartBuffer));
|
|
403
428
|
exports.Set("StartRecording", Napi::Function::New(env, ObsStartRecording));
|
|
404
429
|
exports.Set("StopRecording", Napi::Function::New(env, ObsStopRecording));
|
package/src/obs_interface.cpp
CHANGED
|
@@ -191,55 +191,56 @@ void ObsInterface::init_obs(const std::string& pluginPath, const std::string& da
|
|
|
191
191
|
blog(LOG_INFO, "Exit init_obs");
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
void ObsInterface::create_output(
|
|
195
|
-
blog(LOG_INFO, "Create
|
|
194
|
+
void ObsInterface::create_output() {
|
|
195
|
+
blog(LOG_INFO, "Create outputs");
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
obs_output_release(output);
|
|
200
|
-
}
|
|
197
|
+
blog(LOG_INFO, "Creating replay buffer output");
|
|
198
|
+
buffer_output = obs_output_create("replay_buffer", "Buffer Output", NULL, NULL);
|
|
201
199
|
|
|
202
|
-
if (
|
|
203
|
-
blog(
|
|
204
|
-
|
|
205
|
-
} else {
|
|
206
|
-
blog(LOG_INFO, "Creating file output");
|
|
207
|
-
output = obs_output_create("ffmpeg_muxer", "recording_output", NULL, NULL);
|
|
200
|
+
if (!buffer_output) {
|
|
201
|
+
blog(LOG_ERROR, "Failed to create buffer output!");
|
|
202
|
+
throw std::runtime_error("Failed to create buffer output!");
|
|
208
203
|
}
|
|
209
204
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
blog(LOG_INFO, "Creating file output");
|
|
206
|
+
file_output = obs_output_create("ffmpeg_muxer", "File Output", NULL, NULL);
|
|
207
|
+
|
|
208
|
+
if (!file_output) {
|
|
209
|
+
blog(LOG_ERROR, "Failed to create file output!");
|
|
210
|
+
throw std::runtime_error("Failed to create file output!");
|
|
213
211
|
}
|
|
214
212
|
|
|
215
|
-
obs_data_t *
|
|
213
|
+
obs_data_t *buffer_settings = obs_data_create();
|
|
214
|
+
blog(LOG_INFO, "Set replay_buffer settings");
|
|
215
|
+
obs_data_set_int(buffer_settings, "max_time_sec", 60);
|
|
216
|
+
obs_data_set_int(buffer_settings, "max_size_mb", 1024);
|
|
217
|
+
obs_data_set_string(buffer_settings, "directory", recording_path.c_str());
|
|
218
|
+
obs_data_set_string(buffer_settings, "format", "%CCYY-%MM-%DD %hh-%mm-%ss");
|
|
219
|
+
obs_data_set_string(buffer_settings, "extension", "mp4");
|
|
220
|
+
obs_output_update(buffer_output, buffer_settings);
|
|
221
|
+
obs_data_release(buffer_settings);
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
obs_data_set_string(settings, "extension", "mp4");
|
|
224
|
-
} else {
|
|
225
|
-
blog(LOG_INFO, "Set ffmpeg_muxer settings");
|
|
226
|
-
obs_data_set_string(settings, "extension", "mp4");
|
|
227
|
-
// Apparently need to specify the exact path for ffmpeg_muxer.
|
|
228
|
-
// TODO add something to auto generate this ?
|
|
229
|
-
obs_data_set_string(settings, "path", (recordingPath + "/noobs.mp4").c_str());
|
|
230
|
-
recording_path = recordingPath + "/noobs.mp4";
|
|
231
|
-
}
|
|
223
|
+
blog(LOG_INFO, "Set ffmpeg_muxer settings");
|
|
224
|
+
obs_data_t *ffmpeg_settings = obs_data_create();
|
|
225
|
+
// Need to specify the exact path for ffmpeg_muxer.
|
|
226
|
+
std::string filename = recording_path + "\\" + get_current_date_time() + ".mp4";
|
|
227
|
+
obs_data_set_string(ffmpeg_settings, "path", filename.c_str());
|
|
228
|
+
recording_path = filename;
|
|
232
229
|
|
|
233
230
|
// Apply and release the settings.
|
|
234
|
-
obs_output_update(
|
|
235
|
-
obs_data_release(
|
|
231
|
+
obs_output_update(file_output, ffmpeg_settings);
|
|
232
|
+
obs_data_release(ffmpeg_settings);
|
|
236
233
|
|
|
237
234
|
// Add the signal handler callback.
|
|
238
|
-
|
|
235
|
+
connect_signal_handlers(buffer_output);
|
|
236
|
+
connect_signal_handlers(file_output);
|
|
239
237
|
}
|
|
240
238
|
|
|
241
|
-
void ObsInterface::
|
|
242
|
-
blog(LOG_INFO, "
|
|
239
|
+
void ObsInterface::setRecordingDir(const std::string& recordingPath) {
|
|
240
|
+
blog(LOG_INFO, "Set recording directory");
|
|
241
|
+
// TODO make this work for file output also.
|
|
242
|
+
|
|
243
|
+
obs_output_t *output = buffering ? buffer_output : file_output;
|
|
243
244
|
|
|
244
245
|
if (!output) {
|
|
245
246
|
blog(LOG_ERROR, "No output to update recording directory");
|
|
@@ -267,67 +268,79 @@ void ObsInterface::updateRecordingDir(const std::string& recordingPath) {
|
|
|
267
268
|
void ObsInterface::configure_video_encoder() {
|
|
268
269
|
blog(LOG_INFO, "Create video encoder");
|
|
269
270
|
|
|
270
|
-
|
|
271
|
-
blog(LOG_ERROR, "No output on configure_video_encoder");
|
|
272
|
-
throw std::runtime_error("Failed to create video encoder!");
|
|
273
|
-
}
|
|
271
|
+
file_video_encoder = obs_video_encoder_create("obs_x264", "simple_h264_stream", NULL, NULL);
|
|
274
272
|
|
|
275
|
-
if (video_encoder) {
|
|
276
|
-
blog(LOG_DEBUG, "Releasing video encoder");
|
|
277
|
-
obs_encoder_release(video_encoder);
|
|
278
|
-
video_encoder = nullptr;
|
|
279
|
-
}
|
|
280
273
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (!video_encoder) {
|
|
274
|
+
if (!file_video_encoder) {
|
|
284
275
|
blog(LOG_ERROR, "Failed to create video encoder!");
|
|
285
276
|
throw std::runtime_error("Failed to create video encoder!");
|
|
286
277
|
}
|
|
287
278
|
|
|
288
|
-
|
|
279
|
+
buffer_video_encoder = obs_video_encoder_create("obs_x264", "simple_h264_stream", NULL, NULL);
|
|
280
|
+
|
|
281
|
+
if (!buffer_video_encoder) {
|
|
282
|
+
blog(LOG_ERROR, "Failed to create buffer video encoder!");
|
|
283
|
+
throw std::runtime_error("Failed to create buffer video encoder!");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
blog(LOG_INFO, "Set file video encoder settings");
|
|
289
287
|
obs_data_t* venc_settings = obs_data_create();
|
|
290
288
|
obs_data_set_string(venc_settings, "preset", "speed"); // Faster preset
|
|
291
289
|
obs_data_set_string(venc_settings, "rate_control", "CRF");
|
|
292
290
|
obs_data_set_int(venc_settings, "crf", 30);
|
|
293
291
|
obs_data_set_string(venc_settings, "profile", "main");
|
|
294
292
|
obs_data_set_int(venc_settings, "keyint_sec", 1); // Set keyframe interval to 1 second
|
|
295
|
-
|
|
293
|
+
|
|
294
|
+
obs_encoder_update(file_video_encoder, venc_settings);
|
|
295
|
+
obs_encoder_update(buffer_video_encoder, venc_settings);
|
|
296
296
|
obs_data_release(venc_settings);
|
|
297
|
+
|
|
298
|
+
obs_output_set_video_encoder(file_output, file_video_encoder);
|
|
299
|
+
obs_encoder_set_video(file_video_encoder, obs_get_video());
|
|
300
|
+
obs_output_set_video_encoder(buffer_output, buffer_video_encoder);
|
|
301
|
+
obs_encoder_set_video(buffer_video_encoder, obs_get_video());
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
void ObsInterface::configure_audio_encoder() {
|
|
300
305
|
blog(LOG_INFO, "Create audio encoder");
|
|
301
306
|
|
|
302
|
-
if (!output) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
307
|
+
// if (!output) {
|
|
308
|
+
// blog(LOG_ERROR, "No output on configure_audio_encoder");
|
|
309
|
+
// throw std::runtime_error("Failed to create audio encoder!");
|
|
310
|
+
// }
|
|
306
311
|
|
|
307
|
-
if (audio_encoder) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
+
// if (audio_encoder) {
|
|
313
|
+
// blog(LOG_DEBUG, "Releasing audio encoder");
|
|
314
|
+
// obs_encoder_release(audio_encoder);
|
|
315
|
+
// audio_encoder = nullptr;
|
|
316
|
+
// }
|
|
312
317
|
|
|
313
|
-
|
|
318
|
+
file_audio_encoder = obs_audio_encoder_create("ffmpeg_aac", "simple_aac", NULL, 0, NULL);
|
|
314
319
|
|
|
315
|
-
if (!
|
|
320
|
+
if (!file_audio_encoder) {
|
|
316
321
|
blog(LOG_ERROR, "Failed to create audio encoder!");
|
|
317
322
|
throw std::runtime_error("Failed to create audio encoder!");
|
|
318
323
|
}
|
|
319
324
|
|
|
325
|
+
buffer_audio_encoder = obs_audio_encoder_create("ffmpeg_aac", "simple_aac", NULL, 0, NULL);
|
|
326
|
+
|
|
327
|
+
if (!buffer_audio_encoder) {
|
|
328
|
+
blog(LOG_ERROR, "Failed to create buffer audio encoder!");
|
|
329
|
+
throw std::runtime_error("Failed to create buffer audio encoder!");
|
|
330
|
+
}
|
|
331
|
+
|
|
320
332
|
blog(LOG_INFO, "Set audio encoder settings");
|
|
321
333
|
obs_data_t *aenc_settings = obs_data_create();
|
|
322
334
|
obs_data_set_int(aenc_settings, "bitrate", 128);
|
|
323
|
-
obs_encoder_update(
|
|
335
|
+
obs_encoder_update(file_audio_encoder, aenc_settings);
|
|
336
|
+
obs_encoder_update(buffer_audio_encoder, aenc_settings);
|
|
324
337
|
obs_data_release(aenc_settings);
|
|
325
338
|
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
obs_output_set_audio_encoder(file_output, file_audio_encoder, 0);
|
|
340
|
+
obs_encoder_set_audio(file_audio_encoder, obs_get_audio());
|
|
328
341
|
|
|
329
|
-
|
|
330
|
-
obs_encoder_set_audio(
|
|
342
|
+
obs_output_set_audio_encoder(buffer_output, buffer_audio_encoder, 0);
|
|
343
|
+
obs_encoder_set_audio(buffer_audio_encoder, obs_get_audio());
|
|
331
344
|
}
|
|
332
345
|
|
|
333
346
|
void ObsInterface::create_scene() {
|
|
@@ -471,7 +484,7 @@ void ObsInterface::output_signal_handler_saved(void *data, calldata_t *cd) {
|
|
|
471
484
|
self->jscb.NonBlockingCall(sd, call_jscb);
|
|
472
485
|
}
|
|
473
486
|
|
|
474
|
-
void ObsInterface::
|
|
487
|
+
void ObsInterface::connect_signal_handlers(obs_output_t *output) {
|
|
475
488
|
signal_handler_t *sh = obs_output_get_signal_handler(output);
|
|
476
489
|
signal_handler_connect(sh, "starting", output_signal_handler_starting, this);
|
|
477
490
|
signal_handler_connect(sh, "start", output_signal_handler_start, this);
|
|
@@ -480,69 +493,70 @@ void ObsInterface::create_signal_handlers(obs_output_t *output) {
|
|
|
480
493
|
signal_handler_connect(sh, "saved", output_signal_handler_saved, this);
|
|
481
494
|
}
|
|
482
495
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
496
|
+
void ObsInterface::disconnect_signal_handlers(obs_output_t *output) {
|
|
497
|
+
signal_handler_t *sh = obs_output_get_signal_handler(output);
|
|
498
|
+
signal_handler_disconnect(sh, "starting", output_signal_handler_starting, this);
|
|
499
|
+
signal_handler_disconnect(sh, "start", output_signal_handler_start, this);
|
|
500
|
+
signal_handler_disconnect(sh, "stopping", output_signal_handler_stopping, this);
|
|
501
|
+
signal_handler_disconnect(sh, "stop", output_signal_handler_stop, this);
|
|
502
|
+
signal_handler_disconnect(sh, "saved", output_signal_handler_saved, this);
|
|
503
|
+
}
|
|
489
504
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
505
|
+
bool draw_box(obs_scene_t *scene, obs_sceneitem_t *item, void *p) {
|
|
506
|
+
// Get the item position and size
|
|
507
|
+
vec2 pos; vec2 scale;
|
|
508
|
+
obs_sceneitem_get_pos(item, &pos);
|
|
509
|
+
obs_sceneitem_get_scale(item, &scale);
|
|
493
510
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
511
|
+
// Calculate actual size with scaling
|
|
512
|
+
obs_source_t *src = obs_sceneitem_get_source(item);
|
|
513
|
+
float width = obs_source_get_width(src) * scale.x;
|
|
514
|
+
float height = obs_source_get_height(src) * scale.y;
|
|
497
515
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
516
|
+
// Draw rectangle around the source using the position and size
|
|
517
|
+
gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
|
|
518
|
+
gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color");
|
|
519
|
+
gs_technique_t *tech = gs_effect_get_technique(solid, "Solid");
|
|
502
520
|
|
|
503
|
-
|
|
504
|
-
|
|
521
|
+
vec4 col = {0.733f, 0.267f, 0.125f, 1.0f}; // #BB4420
|
|
522
|
+
gs_effect_set_vec4(color, &col);
|
|
505
523
|
|
|
506
|
-
|
|
507
|
-
|
|
524
|
+
gs_technique_begin(tech);
|
|
525
|
+
gs_technique_begin_pass(tech, 0);
|
|
508
526
|
|
|
509
|
-
|
|
510
|
-
|
|
527
|
+
gs_matrix_push();
|
|
528
|
+
gs_matrix_identity();
|
|
511
529
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
gs_matrix_pop();
|
|
530
|
+
// Top border
|
|
531
|
+
gs_matrix_push();
|
|
532
|
+
gs_matrix_translate3f(pos.x, pos.y, 0.0f);
|
|
533
|
+
gs_draw_sprite(nullptr, 0, width, 2.0f);
|
|
534
|
+
gs_matrix_pop();
|
|
518
535
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
gs_matrix_pop();
|
|
536
|
+
// Bottom border
|
|
537
|
+
gs_matrix_push();
|
|
538
|
+
gs_matrix_translate3f(pos.x, pos.y + height - 2.0f, 0.0f);
|
|
539
|
+
gs_draw_sprite(nullptr, 0, width, 2.0f);
|
|
540
|
+
gs_matrix_pop();
|
|
525
541
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
gs_matrix_pop();
|
|
542
|
+
// Left border
|
|
543
|
+
gs_matrix_push();
|
|
544
|
+
gs_matrix_translate3f(pos.x, pos.y, 0.0f);
|
|
545
|
+
gs_draw_sprite(nullptr, 0, 2.0f, height);
|
|
546
|
+
gs_matrix_pop();
|
|
532
547
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
gs_matrix_pop();
|
|
548
|
+
// Right border
|
|
549
|
+
gs_matrix_push();
|
|
550
|
+
gs_matrix_translate3f(pos.x + width - 2.0f, pos.y, 0.0f);
|
|
551
|
+
gs_draw_sprite(nullptr, 0, 2.0f, height);
|
|
552
|
+
gs_matrix_pop();
|
|
539
553
|
|
|
540
|
-
|
|
554
|
+
gs_matrix_pop();
|
|
541
555
|
|
|
542
|
-
|
|
543
|
-
|
|
556
|
+
gs_technique_end_pass(tech);
|
|
557
|
+
gs_technique_end(tech);
|
|
544
558
|
|
|
545
|
-
|
|
559
|
+
return true;
|
|
546
560
|
}
|
|
547
561
|
|
|
548
562
|
void draw_callback(void* data, uint32_t cx, uint32_t cy) {
|
|
@@ -652,8 +666,7 @@ ObsInterface::ObsInterface(
|
|
|
652
666
|
const std::string& logPath,
|
|
653
667
|
const std::string& dataPath,
|
|
654
668
|
const std::string& recordingPath,
|
|
655
|
-
Napi::ThreadSafeFunction cb
|
|
656
|
-
bool buffering
|
|
669
|
+
Napi::ThreadSafeFunction cb
|
|
657
670
|
) {
|
|
658
671
|
// Setup logs first so we have logs for the initialization.
|
|
659
672
|
base_set_log_handler(log_handler, (void*)logPath.c_str());
|
|
@@ -664,9 +677,10 @@ ObsInterface::ObsInterface(
|
|
|
664
677
|
|
|
665
678
|
// Setup callback function.
|
|
666
679
|
jscb = cb;
|
|
680
|
+
recording_path = recordingPath;
|
|
667
681
|
|
|
668
682
|
// Create the resources we rely on.
|
|
669
|
-
create_output(
|
|
683
|
+
create_output();
|
|
670
684
|
create_scene();
|
|
671
685
|
|
|
672
686
|
configure_video_encoder();
|
|
@@ -691,39 +705,69 @@ ObsInterface::~ObsInterface() {
|
|
|
691
705
|
obs_scene_release(scene);
|
|
692
706
|
}
|
|
693
707
|
|
|
694
|
-
if (
|
|
695
|
-
if (obs_output_active(
|
|
708
|
+
if (buffer_output) {
|
|
709
|
+
if (obs_output_active(buffer_output)) {
|
|
696
710
|
blog(LOG_DEBUG, "Force stopping output");
|
|
697
|
-
obs_output_force_stop(
|
|
711
|
+
obs_output_force_stop(buffer_output);
|
|
698
712
|
}
|
|
699
713
|
|
|
700
714
|
blog(LOG_DEBUG, "Releasing output");
|
|
701
|
-
obs_output_release(
|
|
715
|
+
obs_output_release(buffer_output);
|
|
702
716
|
}
|
|
703
717
|
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
718
|
+
if (file_output) {
|
|
719
|
+
if (obs_output_active(file_output)) {
|
|
720
|
+
blog(LOG_DEBUG, "Force stopping output");
|
|
721
|
+
obs_output_force_stop(file_output);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
blog(LOG_DEBUG, "Releasing output");
|
|
725
|
+
obs_output_release(file_output);
|
|
707
726
|
}
|
|
708
727
|
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
728
|
+
// if (video_encoder) {
|
|
729
|
+
// blog(LOG_DEBUG, "Releasing video encoder");
|
|
730
|
+
// obs_encoder_release(video_encoder);
|
|
731
|
+
// }
|
|
732
|
+
|
|
733
|
+
// if (audio_encoder) {
|
|
734
|
+
// blog(LOG_DEBUG, "Releasing audio encoder");
|
|
735
|
+
// obs_encoder_release(audio_encoder);
|
|
736
|
+
// }
|
|
713
737
|
|
|
714
738
|
blog(LOG_DEBUG, "Now shutting down OBS");
|
|
715
739
|
obs_shutdown();
|
|
716
740
|
}
|
|
717
741
|
|
|
742
|
+
bool ObsInterface::setBuffering(bool value) {
|
|
743
|
+
obs_output_t* output = buffering ? buffer_output : file_output;
|
|
744
|
+
|
|
745
|
+
if (obs_output_active(output)) {
|
|
746
|
+
blog(LOG_ERROR, "Cannot change buffering state while output is active");
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
buffering = value;
|
|
751
|
+
return buffering;
|
|
752
|
+
}
|
|
753
|
+
|
|
718
754
|
void ObsInterface::startBuffering() {
|
|
719
755
|
blog(LOG_INFO, "ObsInterface::startBuffering called");
|
|
720
756
|
|
|
757
|
+
if (!buffering) {
|
|
758
|
+
blog(LOG_ERROR, "Buffering is not enabled!");
|
|
759
|
+
throw std::runtime_error("Buffering is not enabled!");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
obs_output_t* output = buffer_output;
|
|
763
|
+
|
|
721
764
|
if (!output) {
|
|
765
|
+
blog(LOG_ERROR, "Output is not initialized!");
|
|
722
766
|
throw std::runtime_error("Output is not initialized!");
|
|
723
767
|
}
|
|
724
768
|
|
|
725
769
|
bool is_active = obs_output_active(output);
|
|
726
|
-
|
|
770
|
+
|
|
727
771
|
if (is_active) {
|
|
728
772
|
blog(LOG_WARNING, "Output is already active");
|
|
729
773
|
return;
|
|
@@ -732,6 +776,7 @@ void ObsInterface::startBuffering() {
|
|
|
732
776
|
bool success = obs_output_start(output);
|
|
733
777
|
|
|
734
778
|
if (!success) {
|
|
779
|
+
blog(LOG_ERROR, "Failed to start buffering!");
|
|
735
780
|
throw std::runtime_error("Failed to start buffering!");
|
|
736
781
|
}
|
|
737
782
|
|
|
@@ -740,10 +785,9 @@ void ObsInterface::startBuffering() {
|
|
|
740
785
|
|
|
741
786
|
void ObsInterface::startRecording(int offset) {
|
|
742
787
|
blog(LOG_INFO, "ObsInterface::startRecording enter");
|
|
788
|
+
obs_output_t* output = buffering ? buffer_output : file_output;
|
|
743
789
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (strcmp(type, "replay_buffer") == 0) {
|
|
790
|
+
if (buffering) {
|
|
747
791
|
bool is_active = obs_output_active(output);
|
|
748
792
|
|
|
749
793
|
if (!is_active) {
|
|
@@ -762,7 +806,7 @@ void ObsInterface::startRecording(int offset) {
|
|
|
762
806
|
if (!success) {
|
|
763
807
|
throw std::runtime_error("Failed to call convert procedure handler");
|
|
764
808
|
}
|
|
765
|
-
} else
|
|
809
|
+
} else {
|
|
766
810
|
blog(LOG_INFO, "Starting ffmpeg_muxer output");
|
|
767
811
|
|
|
768
812
|
bool is_active = obs_output_active(output);
|
|
@@ -780,9 +824,6 @@ void ObsInterface::startRecording(int offset) {
|
|
|
780
824
|
blog(LOG_ERROR, "Failed to start recording: %s", err ? err : "Unknown error");
|
|
781
825
|
throw std::runtime_error("Failed to start recording");
|
|
782
826
|
}
|
|
783
|
-
} else {
|
|
784
|
-
blog(LOG_ERROR, "Unknown output type: %s", type);
|
|
785
|
-
throw std::runtime_error("Unknown output type!");
|
|
786
827
|
}
|
|
787
828
|
|
|
788
829
|
blog(LOG_INFO, "ObsInterface::startRecording exit");
|
|
@@ -790,10 +831,11 @@ void ObsInterface::startRecording(int offset) {
|
|
|
790
831
|
|
|
791
832
|
void ObsInterface::stopRecording() {
|
|
792
833
|
blog(LOG_INFO, "ObsInterface::stopRecording enter");
|
|
834
|
+
obs_output_t* output = buffering ? buffer_output : file_output;
|
|
793
835
|
bool is_active = obs_output_active(output);
|
|
794
836
|
|
|
795
837
|
if (!is_active) {
|
|
796
|
-
blog(LOG_WARNING, "
|
|
838
|
+
blog(LOG_WARNING, "Output is not active");
|
|
797
839
|
return;
|
|
798
840
|
}
|
|
799
841
|
|
|
@@ -805,22 +847,18 @@ std::string ObsInterface::getLastRecording() {
|
|
|
805
847
|
blog(LOG_INFO, "calling get last replay proc handler");
|
|
806
848
|
calldata cd;
|
|
807
849
|
calldata_init(&cd);
|
|
850
|
+
|
|
851
|
+
obs_output_t* output = buffering ? buffer_output : file_output;
|
|
808
852
|
proc_handler_t *ph = obs_output_get_proc_handler(output);
|
|
809
853
|
|
|
810
854
|
const char* type = obs_output_get_id(output);
|
|
811
855
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (strcmp(type, "ffmpeg_muxer") == 0) {
|
|
856
|
+
if (!buffering) {
|
|
857
|
+
blog(LOG_INFO, "Getting last recording path from ffmpeg_muxer");
|
|
815
858
|
return recording_path;
|
|
816
859
|
}
|
|
817
860
|
|
|
818
|
-
bool success;
|
|
819
|
-
|
|
820
|
-
if (strcmp(type, "replay_buffer") != 0) {
|
|
821
|
-
blog(LOG_ERROR, "Unknown output type: %s", type);
|
|
822
|
-
throw std::runtime_error("Unknown output type!");
|
|
823
|
-
}
|
|
861
|
+
bool success = proc_handler_call(ph, "get_last_replay", &cd);
|
|
824
862
|
|
|
825
863
|
if (!success) {
|
|
826
864
|
blog(LOG_ERROR, "Failed to call procedure handler");
|
package/src/obs_interface.h
CHANGED
|
@@ -14,17 +14,17 @@ class ObsInterface {
|
|
|
14
14
|
const std::string& logPath, // Where to write logs to
|
|
15
15
|
const std::string& dataPath, // Where to look for effects
|
|
16
16
|
const std::string& recordingPath, // Where to save recordings
|
|
17
|
-
Napi::ThreadSafeFunction cb
|
|
18
|
-
bool buffering // Whether to enable buffering the recording in memory
|
|
17
|
+
Napi::ThreadSafeFunction cb // JavaScript callback
|
|
19
18
|
);
|
|
20
19
|
|
|
21
20
|
~ObsInterface();
|
|
22
21
|
|
|
22
|
+
bool setBuffering(bool buffering); // In buffering mode, the recording is stored in memory and can be converted to a file later.
|
|
23
23
|
void startBuffering(); // Start buffering to memory.
|
|
24
24
|
void startRecording(int offset); // Convert the active buffered recording to a real one.
|
|
25
25
|
void stopRecording(); // Stop the recording.
|
|
26
26
|
std::string getLastRecording(); // Get the last recorded file path.
|
|
27
|
-
void
|
|
27
|
+
void setRecordingDir(const std::string& recordingPath); // Output must not be active when calling this.
|
|
28
28
|
|
|
29
29
|
void createSource(std::string name, std::string type); // Create a new source
|
|
30
30
|
void deleteSource(std::string name); // Release a source.
|
|
@@ -51,22 +51,27 @@ class ObsInterface {
|
|
|
51
51
|
// Reconfigure video sources
|
|
52
52
|
|
|
53
53
|
private:
|
|
54
|
-
obs_output_t *
|
|
54
|
+
obs_output_t *file_output = nullptr;
|
|
55
|
+
obs_output_t *buffer_output = nullptr;
|
|
55
56
|
obs_scene_t *scene = nullptr;
|
|
56
57
|
obs_source_t *video_source = nullptr;
|
|
57
58
|
obs_source_t *audio_source = nullptr;
|
|
58
|
-
obs_encoder_t *
|
|
59
|
-
obs_encoder_t *
|
|
59
|
+
obs_encoder_t *file_video_encoder = nullptr;
|
|
60
|
+
obs_encoder_t *file_audio_encoder = nullptr;
|
|
61
|
+
obs_encoder_t *buffer_video_encoder = nullptr;
|
|
62
|
+
obs_encoder_t *buffer_audio_encoder = nullptr;
|
|
60
63
|
obs_display_t *display = nullptr;
|
|
61
64
|
HWND preview_hwnd = nullptr; // window handle for scene preview
|
|
62
65
|
Napi::ThreadSafeFunction jscb; // javascript callback
|
|
63
66
|
std::string recording_path = "";
|
|
67
|
+
bool buffering = false; // Whether we are buffering the recording in memory.
|
|
64
68
|
|
|
65
69
|
void init_obs(const std::string& pluginPath, const std::string& dataPath);
|
|
66
70
|
void reset_video();
|
|
67
71
|
void reset_audio();
|
|
68
72
|
void load_module(const char* module);
|
|
69
|
-
void
|
|
73
|
+
void connect_signal_handlers(obs_output_t *output);
|
|
74
|
+
void disconnect_signal_handlers(obs_output_t *output);
|
|
70
75
|
|
|
71
76
|
static void output_signal_handler_starting(void *data, calldata_t *cd);
|
|
72
77
|
static void output_signal_handler_start(void *data, calldata_t *cd);
|
|
@@ -80,7 +85,7 @@ class ObsInterface {
|
|
|
80
85
|
void list_output_types();
|
|
81
86
|
|
|
82
87
|
void create_scene();
|
|
83
|
-
void create_output(
|
|
88
|
+
void create_output();
|
|
84
89
|
|
|
85
90
|
void configure_video_encoder();
|
|
86
91
|
void configure_audio_encoder();
|
package/src/utils.cpp
CHANGED
|
@@ -394,3 +394,11 @@ Napi::Object property_to_napi(Napi::Env env, obs_property_t* property) {
|
|
|
394
394
|
|
|
395
395
|
return obj;
|
|
396
396
|
}
|
|
397
|
+
|
|
398
|
+
std::string get_current_date_time() {
|
|
399
|
+
auto now = std::chrono::system_clock::now();
|
|
400
|
+
auto time_t = std::chrono::system_clock::to_time_t(now);
|
|
401
|
+
std::stringstream ss;
|
|
402
|
+
ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H-%M-%S");
|
|
403
|
+
return ss.str();
|
|
404
|
+
}
|
package/src/utils.h
CHANGED
|
@@ -9,4 +9,5 @@ Napi::Object data_to_napi(Napi::Env env,obs_data_t* data);
|
|
|
9
9
|
obs_data_t* napi_to_data(Napi::Object obj);
|
|
10
10
|
|
|
11
11
|
Napi::Object property_to_napi(Napi::Env env, obs_property_t* property);
|
|
12
|
-
Napi::Array properties_to_napi(Napi::Env env, obs_properties_t* properties);
|
|
12
|
+
Napi::Array properties_to_napi(Napi::Env env, obs_properties_t* properties);
|
|
13
|
+
std::string get_current_date_time();
|