node-mac-recorder 2.1.3 → 2.4.0
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/.claude/settings.local.json +6 -1
- package/README.md +17 -79
- package/backup/binding.gyp +44 -0
- package/backup/src/audio_capture.mm +116 -0
- package/backup/src/cursor_tracker.mm +518 -0
- package/backup/src/mac_recorder.mm +829 -0
- package/backup/src/screen_capture.h +19 -0
- package/backup/src/screen_capture.mm +162 -0
- package/backup/src/screen_capture_kit.h +15 -0
- package/backup/src/window_selector.mm +1457 -0
- package/binding.gyp +15 -10
- package/index.js +22 -62
- package/package.json +3 -8
- package/src/audio_capture.mm +96 -40
- package/src/cursor_tracker.mm +3 -4
- package/src/mac_recorder.mm +604 -715
- package/src/screen_capture.h +5 -0
- package/src/screen_capture.mm +141 -60
- package/src/window_selector.mm +12 -22
- package/test-api-compatibility.js +92 -0
- package/test-audio.js +94 -0
- package/test-comprehensive.js +164 -0
- package/test-recording.js +142 -0
- package/test-sck-simple.js +37 -0
- package/test-sck.js +50 -44
- package/test-sync.js +52 -0
- package/test-windows.js +57 -0
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/prebuilds/darwin-x64/node.napi.node +0 -0
- package/scripts/test-exclude.js +0 -72
- package/src/screen_capture_kit.mm +0 -222
package/binding.gyp
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"sources": [
|
|
6
6
|
"src/mac_recorder.mm",
|
|
7
7
|
"src/screen_capture.mm",
|
|
8
|
-
"src/screen_capture_kit.mm",
|
|
9
8
|
"src/audio_capture.mm",
|
|
10
9
|
"src/cursor_tracker.mm",
|
|
11
10
|
"src/window_selector.mm"
|
|
@@ -22,25 +21,31 @@
|
|
|
22
21
|
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
23
22
|
"CLANG_CXX_LIBRARY": "libc++",
|
|
24
23
|
"MACOSX_DEPLOYMENT_TARGET": "12.3",
|
|
24
|
+
"ARCHS": ["arm64"],
|
|
25
|
+
"VALID_ARCHS": ["arm64"],
|
|
25
26
|
"OTHER_CFLAGS": [
|
|
26
|
-
"-ObjC++"
|
|
27
|
-
|
|
27
|
+
"-ObjC++",
|
|
28
|
+
"-fmodules"
|
|
29
|
+
],
|
|
30
|
+
"CLANG_ENABLE_OBJC_ARC": "YES"
|
|
28
31
|
},
|
|
29
32
|
"link_settings": {
|
|
30
33
|
"libraries": [
|
|
34
|
+
"-framework ScreenCaptureKit",
|
|
35
|
+
"-framework AVFoundation",
|
|
36
|
+
"-framework CoreMedia",
|
|
37
|
+
"-framework CoreVideo",
|
|
31
38
|
"-framework Foundation",
|
|
32
39
|
"-framework AppKit",
|
|
33
|
-
"-framework ScreenCaptureKit",
|
|
34
40
|
"-framework ApplicationServices",
|
|
35
41
|
"-framework Carbon",
|
|
36
|
-
"-framework Accessibility"
|
|
37
|
-
"-framework CoreAudio",
|
|
38
|
-
"-framework AVFoundation",
|
|
39
|
-
"-framework CoreMedia",
|
|
40
|
-
"-framework CoreVideo"
|
|
42
|
+
"-framework Accessibility"
|
|
41
43
|
]
|
|
42
44
|
},
|
|
43
|
-
"defines": [
|
|
45
|
+
"defines": [
|
|
46
|
+
"NAPI_DISABLE_CPP_EXCEPTIONS",
|
|
47
|
+
"USE_SCREENCAPTUREKIT=1"
|
|
48
|
+
]
|
|
44
49
|
}
|
|
45
50
|
]
|
|
46
51
|
}
|
package/index.js
CHANGED
|
@@ -2,24 +2,19 @@ const { EventEmitter } = require("events");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
|
|
5
|
-
// Native modülü yükle
|
|
5
|
+
// Native modülü yükle
|
|
6
6
|
let nativeBinding;
|
|
7
7
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} catch (e) {
|
|
8
|
+
nativeBinding = require("./build/Release/mac_recorder.node");
|
|
9
|
+
} catch (error) {
|
|
11
10
|
try {
|
|
12
|
-
nativeBinding = require("./build/
|
|
13
|
-
} catch (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"Original error: " +
|
|
20
|
-
(error?.message || e?.message)
|
|
21
|
-
);
|
|
22
|
-
}
|
|
11
|
+
nativeBinding = require("./build/Debug/mac_recorder.node");
|
|
12
|
+
} catch (debugError) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'Native module not found. Please run "npm run build" to compile the native module.\n' +
|
|
15
|
+
"Original error: " +
|
|
16
|
+
error.message
|
|
17
|
+
);
|
|
23
18
|
}
|
|
24
19
|
}
|
|
25
20
|
|
|
@@ -50,12 +45,6 @@ class MacRecorder extends EventEmitter {
|
|
|
50
45
|
showClicks: false,
|
|
51
46
|
displayId: null, // Hangi ekranı kaydedeceği (null = ana ekran)
|
|
52
47
|
windowId: null, // Hangi pencereyi kaydedeceği (null = tam ekran)
|
|
53
|
-
// SC (gizli, opsiyonel) - mevcut kullanıcıları bozmaz
|
|
54
|
-
useScreenCaptureKit: false,
|
|
55
|
-
excludedAppBundleIds: [],
|
|
56
|
-
excludedPIDs: [],
|
|
57
|
-
excludedWindowIds: [],
|
|
58
|
-
autoExcludeSelf: !!(process.versions && process.versions.electron),
|
|
59
48
|
};
|
|
60
49
|
|
|
61
50
|
// Display cache için async initialization
|
|
@@ -129,14 +118,11 @@ class MacRecorder extends EventEmitter {
|
|
|
129
118
|
audioDeviceId: options.audioDeviceId || null, // null = default device
|
|
130
119
|
systemAudioDeviceId: options.systemAudioDeviceId || null, // null = auto-detect system audio device
|
|
131
120
|
captureArea: options.captureArea || null,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
typeof options.autoExcludeSelf === "boolean"
|
|
138
|
-
? options.autoExcludeSelf
|
|
139
|
-
: !!(process.versions && process.versions.electron),
|
|
121
|
+
// Exclusion options
|
|
122
|
+
excludeCurrentApp: options.excludeCurrentApp || false,
|
|
123
|
+
excludeWindowIds: Array.isArray(options.excludeWindowIds)
|
|
124
|
+
? options.excludeWindowIds
|
|
125
|
+
: [],
|
|
140
126
|
};
|
|
141
127
|
}
|
|
142
128
|
|
|
@@ -297,11 +283,9 @@ class MacRecorder extends EventEmitter {
|
|
|
297
283
|
windowId: this.options.windowId || null, // null = tam ekran
|
|
298
284
|
audioDeviceId: this.options.audioDeviceId || null, // null = default device
|
|
299
285
|
systemAudioDeviceId: this.options.systemAudioDeviceId || null, // null = auto-detect system audio device
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
excludedWindowIds: this.options.excludedWindowIds || [],
|
|
304
|
-
autoExcludeSelf: this.options.autoExcludeSelf === true,
|
|
286
|
+
// Exclusion options passthrough
|
|
287
|
+
excludeCurrentApp: this.options.excludeCurrentApp || false,
|
|
288
|
+
excludeWindowIds: this.options.excludeWindowIds || [],
|
|
305
289
|
};
|
|
306
290
|
|
|
307
291
|
// Manuel captureArea varsa onu kullan
|
|
@@ -314,34 +298,10 @@ class MacRecorder extends EventEmitter {
|
|
|
314
298
|
};
|
|
315
299
|
}
|
|
316
300
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.options.useScreenCaptureKit ||
|
|
322
|
-
this.options.excludedAppBundleIds?.length ||
|
|
323
|
-
this.options.excludedPIDs?.length ||
|
|
324
|
-
this.options.excludedWindowIds?.length
|
|
325
|
-
);
|
|
326
|
-
const scAvailable =
|
|
327
|
-
typeof nativeBinding.isScreenCaptureKitAvailable === "function" &&
|
|
328
|
-
nativeBinding.isScreenCaptureKitAvailable();
|
|
329
|
-
if (wantsSC && scAvailable) {
|
|
330
|
-
const scOptions = {
|
|
331
|
-
...recordingOptions,
|
|
332
|
-
useScreenCaptureKit: true,
|
|
333
|
-
};
|
|
334
|
-
success = nativeBinding.startRecording(outputPath, scOptions);
|
|
335
|
-
} else {
|
|
336
|
-
success = nativeBinding.startRecording(
|
|
337
|
-
outputPath,
|
|
338
|
-
recordingOptions
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
// Fallback AVFoundation
|
|
343
|
-
success = nativeBinding.startRecording(outputPath, recordingOptions);
|
|
344
|
-
}
|
|
301
|
+
const success = nativeBinding.startRecording(
|
|
302
|
+
outputPath,
|
|
303
|
+
recordingOptions
|
|
304
|
+
);
|
|
345
305
|
|
|
346
306
|
if (success) {
|
|
347
307
|
this.isRecording = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-mac-recorder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Native macOS screen recording package for Node.js applications",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -38,19 +38,14 @@
|
|
|
38
38
|
"build": "node-gyp build",
|
|
39
39
|
"rebuild": "node-gyp rebuild",
|
|
40
40
|
"clean": "node-gyp clean",
|
|
41
|
-
"prebuild:darwin:arm64": "prebuildify --napi --strip --platform darwin --arch arm64",
|
|
42
|
-
"prebuild:darwin:x64": "prebuildify --napi --strip --platform darwin --arch x64",
|
|
43
|
-
"prebuild": "npm run prebuild:darwin:arm64 && npm run prebuild:darwin:x64",
|
|
44
41
|
"test:window-selector": "node window-selector-test.js",
|
|
45
42
|
"example:window-selector": "node examples/window-selector-example.js"
|
|
46
43
|
},
|
|
47
44
|
"dependencies": {
|
|
48
|
-
"node-addon-api": "^7.0.0"
|
|
49
|
-
"node-gyp-build": "^4.6.0"
|
|
45
|
+
"node-addon-api": "^7.0.0"
|
|
50
46
|
},
|
|
51
47
|
"devDependencies": {
|
|
52
|
-
"node-gyp": "^10.0.0"
|
|
53
|
-
"prebuildify": "^5.0.0"
|
|
48
|
+
"node-gyp": "^10.0.0"
|
|
54
49
|
},
|
|
55
50
|
"gypfile": true
|
|
56
51
|
}
|
package/src/audio_capture.mm
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
1
2
|
#import <AVFoundation/AVFoundation.h>
|
|
2
3
|
#import <CoreAudio/CoreAudio.h>
|
|
3
4
|
|
|
4
5
|
@interface AudioCapture : NSObject
|
|
5
6
|
|
|
6
7
|
+ (NSArray *)getAudioDevices;
|
|
8
|
+
+ (NSArray *)getSystemAudioDevices;
|
|
7
9
|
+ (BOOL)hasAudioPermission;
|
|
8
10
|
+ (void)requestAudioPermission:(void(^)(BOOL granted))completion;
|
|
9
11
|
|
|
@@ -14,25 +16,39 @@
|
|
|
14
16
|
+ (NSArray *)getAudioDevices {
|
|
15
17
|
NSMutableArray *devices = [NSMutableArray array];
|
|
16
18
|
|
|
17
|
-
// Get
|
|
18
|
-
|
|
19
|
+
// Get microphone devices using AVFoundation
|
|
20
|
+
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
|
|
21
|
+
discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone, AVCaptureDeviceTypeExternalUnknown]
|
|
22
|
+
mediaType:AVMediaTypeAudio
|
|
23
|
+
position:AVCaptureDevicePositionUnspecified];
|
|
24
|
+
NSArray *audioDevices = discoverySession.devices;
|
|
19
25
|
|
|
20
26
|
for (AVCaptureDevice *device in audioDevices) {
|
|
21
27
|
NSDictionary *deviceInfo = @{
|
|
22
28
|
@"id": device.uniqueID,
|
|
23
29
|
@"name": device.localizedName,
|
|
24
30
|
@"manufacturer": device.manufacturer ?: @"Unknown",
|
|
31
|
+
@"type": @"microphone",
|
|
25
32
|
@"isDefault": @([device isEqual:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]])
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
[devices addObject:deviceInfo];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
//
|
|
38
|
+
// Add system audio devices using Core Audio API
|
|
39
|
+
NSArray *systemDevices = [self getSystemAudioDevices];
|
|
40
|
+
[devices addObjectsFromArray:systemDevices];
|
|
41
|
+
|
|
42
|
+
return [devices copy];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
+ (NSArray *)getSystemAudioDevices {
|
|
46
|
+
NSMutableArray *devices = [NSMutableArray array];
|
|
47
|
+
|
|
32
48
|
AudioObjectPropertyAddress propertyAddress = {
|
|
33
49
|
kAudioHardwarePropertyDevices,
|
|
34
50
|
kAudioObjectPropertyScopeGlobal,
|
|
35
|
-
kAudioObjectPropertyElementMaster
|
|
51
|
+
kAudioObjectPropertyElementMain // Changed from kAudioObjectPropertyElementMaster (deprecated)
|
|
36
52
|
};
|
|
37
53
|
|
|
38
54
|
UInt32 dataSize = 0;
|
|
@@ -49,37 +65,41 @@
|
|
|
49
65
|
AudioDeviceID deviceID = audioDeviceIDs[i];
|
|
50
66
|
|
|
51
67
|
// Get device name
|
|
52
|
-
propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
|
|
53
|
-
propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
|
|
54
|
-
|
|
55
68
|
CFStringRef deviceName = NULL;
|
|
56
|
-
|
|
69
|
+
UInt32 size = sizeof(deviceName);
|
|
70
|
+
AudioObjectPropertyAddress nameAddress = {
|
|
71
|
+
kAudioDevicePropertyDeviceNameCFString,
|
|
72
|
+
kAudioDevicePropertyScopeOutput, // Focus on output devices for system audio
|
|
73
|
+
kAudioObjectPropertyElementMain
|
|
74
|
+
};
|
|
57
75
|
|
|
58
|
-
status = AudioObjectGetPropertyData(deviceID, &
|
|
76
|
+
status = AudioObjectGetPropertyData(deviceID, &nameAddress, 0, NULL, &size, &deviceName);
|
|
59
77
|
|
|
60
78
|
if (status == kAudioHardwareNoError && deviceName) {
|
|
61
|
-
// Check if
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
// Check if this is an output device
|
|
80
|
+
AudioObjectPropertyAddress streamAddress = {
|
|
81
|
+
kAudioDevicePropertyStreams,
|
|
82
|
+
kAudioDevicePropertyScopeOutput,
|
|
83
|
+
kAudioObjectPropertyElementMain
|
|
84
|
+
};
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
UInt32 streamSize = 0;
|
|
87
|
+
status = AudioObjectGetPropertyDataSize(deviceID, &streamAddress, 0, NULL, &streamSize);
|
|
66
88
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
if (status == kAudioHardwareNoError && streamSize > 0) {
|
|
90
|
+
// This is an output device - can be used for system audio capture
|
|
91
|
+
const char *name = CFStringGetCStringPtr(deviceName, kCFStringEncodingUTF8);
|
|
92
|
+
NSString *deviceNameStr = name ? [NSString stringWithUTF8String:name] : @"Unknown Device";
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
[devices addObject:deviceInfo];
|
|
80
|
-
}
|
|
94
|
+
NSDictionary *deviceInfo = @{
|
|
95
|
+
@"id": [NSString stringWithFormat:@"%u", deviceID],
|
|
96
|
+
@"name": deviceNameStr,
|
|
97
|
+
@"manufacturer": @"System",
|
|
98
|
+
@"type": @"system_audio",
|
|
99
|
+
@"isDefault": @(NO) // We'll determine default separately if needed
|
|
100
|
+
};
|
|
81
101
|
|
|
82
|
-
|
|
102
|
+
[devices addObject:deviceInfo];
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
CFRelease(deviceName);
|
|
@@ -94,23 +114,59 @@
|
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
+ (BOOL)hasAudioPermission {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
return YES; // Older versions don't require explicit permission
|
|
117
|
+
// Check microphone permission using AVFoundation
|
|
118
|
+
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
|
119
|
+
return authStatus == AVAuthorizationStatusAuthorized;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
+ (void)requestAudioPermission:(void(^)(BOOL granted))completion {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
|
|
124
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
125
|
+
if (completion) {
|
|
108
126
|
completion(granted);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@end
|
|
133
|
+
|
|
134
|
+
// ScreenCaptureKit Audio Configuration Helper
|
|
135
|
+
API_AVAILABLE(macos(12.3))
|
|
136
|
+
@interface SCKAudioConfiguration : NSObject
|
|
137
|
+
|
|
138
|
+
+ (BOOL)configureAudioForStream:(SCStreamConfiguration *)config
|
|
139
|
+
includeMicrophone:(BOOL)includeMicrophone
|
|
140
|
+
includeSystemAudio:(BOOL)includeSystemAudio
|
|
141
|
+
microphoneDevice:(NSString *)micDeviceID
|
|
142
|
+
systemAudioDevice:(NSString *)sysDeviceID;
|
|
143
|
+
|
|
144
|
+
@end
|
|
145
|
+
|
|
146
|
+
@implementation SCKAudioConfiguration
|
|
147
|
+
|
|
148
|
+
+ (BOOL)configureAudioForStream:(SCStreamConfiguration *)config
|
|
149
|
+
includeMicrophone:(BOOL)includeMicrophone
|
|
150
|
+
includeSystemAudio:(BOOL)includeSystemAudio
|
|
151
|
+
microphoneDevice:(NSString *)micDeviceID
|
|
152
|
+
systemAudioDevice:(NSString *)sysDeviceID {
|
|
153
|
+
|
|
154
|
+
// Configure system audio capture (requires macOS 13.0+)
|
|
155
|
+
if (@available(macOS 13.0, *)) {
|
|
156
|
+
config.capturesAudio = includeSystemAudio;
|
|
157
|
+
config.excludesCurrentProcessAudio = YES;
|
|
158
|
+
|
|
159
|
+
if (includeSystemAudio) {
|
|
160
|
+
// ScreenCaptureKit will capture system audio from the selected content
|
|
161
|
+
// Quality settings
|
|
162
|
+
config.channelCount = 2; // Stereo
|
|
163
|
+
config.sampleRate = 48000; // 48kHz
|
|
164
|
+
} else {
|
|
165
|
+
config.capturesAudio = NO;
|
|
166
|
+
}
|
|
113
167
|
}
|
|
168
|
+
|
|
169
|
+
return YES;
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
@end
|
|
172
|
+
@end
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -168,7 +168,7 @@ void writeToFile(NSDictionary *cursorData) {
|
|
|
168
168
|
options:0
|
|
169
169
|
error:&error];
|
|
170
170
|
if (jsonData && !error) {
|
|
171
|
-
NSString *jsonString = [[
|
|
171
|
+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
172
172
|
|
|
173
173
|
if (g_isFirstWrite) {
|
|
174
174
|
// İlk yazma - array başlat
|
|
@@ -292,7 +292,6 @@ void cleanupCursorTracking() {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
if (g_timerTarget) {
|
|
295
|
-
[g_timerTarget autorelease];
|
|
296
295
|
g_timerTarget = nil;
|
|
297
296
|
}
|
|
298
297
|
|
|
@@ -352,12 +351,12 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
352
351
|
@try {
|
|
353
352
|
// Dosyayı oluştur ve aç
|
|
354
353
|
g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
355
|
-
g_fileHandle = [
|
|
354
|
+
g_fileHandle = [NSFileHandle fileHandleForWritingAtPath:g_outputPath];
|
|
356
355
|
|
|
357
356
|
if (!g_fileHandle) {
|
|
358
357
|
// Dosya yoksa oluştur
|
|
359
358
|
[[NSFileManager defaultManager] createFileAtPath:g_outputPath contents:nil attributes:nil];
|
|
360
|
-
g_fileHandle = [
|
|
359
|
+
g_fileHandle = [NSFileHandle fileHandleForWritingAtPath:g_outputPath];
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
if (!g_fileHandle) {
|