Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions lib/installer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const crypto = require('crypto');
const { execFileSync } = require('child_process');
const fs = require('node:fs');
const path = require('node:path');
const https = require('node:https');
const crypto = require('node:crypto');
const { execFileSync } = require('node:child_process');
const tar = require('tar');

const MACOS_APP_BUNDLE = 'VideoDBCapture.app';
Expand Down
14 changes: 7 additions & 7 deletions src/capture/binaryManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn, type ChildProcess } from 'child_process';
import * as readline from 'readline';
import { EventEmitter } from 'events';
import { spawn, type ChildProcess } from 'node:child_process';
import * as readline from 'node:readline';
import { EventEmitter } from 'node:events';
import { v4 as uuidv4 } from 'uuid';
import {
PROTOCOL_PREFIX,
Expand Down Expand Up @@ -49,7 +49,7 @@ export class BinaryManager extends EventEmitter {
private getBinaryCommand(): { command: string; args: string[] } {
if (this.isDev) {
// In dev mode, use a mock binary (Node.js script)
const path = require('path');
const path = require('node:path');
return {
command: 'node',
args: [path.join(__dirname, '..', '..', 'mock', 'binary.js')],
Expand Down Expand Up @@ -88,7 +88,7 @@ export class BinaryManager extends EventEmitter {
*/
private handleMessage(msg: BinaryMessage): void {
if (msg.type === 'response') {
const response = msg as BinaryResponse;
const response = msg;
const { commandId, status, result } = response;
const promise = this.pendingCommands.get(commandId);
if (promise) {
Expand All @@ -100,7 +100,7 @@ export class BinaryManager extends EventEmitter {
this.pendingCommands.delete(commandId);
}
} else if (msg.type === 'event') {
const event = msg as BinaryEvent;
const event = msg;
this.emit(event.event, event.payload);
}
}
Expand Down Expand Up @@ -301,7 +301,7 @@ export class BinaryManager extends EventEmitter {
resolve: resolve as (value: unknown) => void,
reject,
});
this.process!.stdin!.write(payload, (err) => {
this.process!.stdin!.write(payload, err => {
if (err) {
const pending = this.pendingCommands.get(commandId);
if (pending) {
Expand Down
7 changes: 4 additions & 3 deletions src/capture/captureClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventEmitter } from 'events';
import { EventEmitter } from 'node:events';
import { BinaryManager } from './binaryManager';
import {
PermissionType,
Expand Down Expand Up @@ -244,8 +244,9 @@ export class CaptureClient extends EventEmitter implements ChannelClient {
throw new Error('channels must include channelId for each channel');
}

const primaryVideo = channels.find(ch => ch.is_primary && ch.type === 'video')
|| channels.find(ch => ch.type === 'video');
const primaryVideo =
channels.find(ch => ch.is_primary && ch.type === 'video') ||
channels.find(ch => ch.type === 'video');

await this.binaryManager.sendCommand('startRecording', {
uploadToken: this.sessionToken,
Expand Down
27 changes: 16 additions & 11 deletions src/capture/installer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as crypto from 'crypto';
import { execFileSync } from 'child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as https from 'node:https';
import * as crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
import type { BinaryConfig, PlatformInfo } from './types';

/** Name of the macOS .app bundle that wraps the capture binary for TCC compatibility */
Expand Down Expand Up @@ -39,9 +39,12 @@ export class RecorderInstaller {
baseUrl: 'https://artifacts.videodb.io/capture',
version: '0.3.1',
checksums: {
'darwin-x64': '8b456607ba3628092081d92c1a22fcf4e8156f4e83b2d3d119bf0244eaa870b2',
'darwin-arm64': 'cefc35883acd53f63dc50f8deb186ea0a8e17c65e646af8e81a309251a220b9d',
'win32-x64': 'e388639c15ab35ac32179d3fc05a363f0f71d4d90265c6b48fb1af56ecae7736',
'darwin-x64':
'8b456607ba3628092081d92c1a22fcf4e8156f4e83b2d3d119bf0244eaa870b2',
'darwin-arm64':
'cefc35883acd53f63dc50f8deb186ea0a8e17c65e646af8e81a309251a220b9d',
'win32-x64':
'e388639c15ab35ac32179d3fc05a363f0f71d4d90265c6b48fb1af56ecae7736',
},
};

Expand Down Expand Up @@ -93,7 +96,7 @@ export class RecorderInstaller {
MACOS_APP_BUNDLE,
'Contents',
'MacOS',
'capture',
'capture'
);
} else if (process.platform === 'win32') {
binPath = path.join(this.binDir, 'capture.exe');
Expand Down Expand Up @@ -305,7 +308,9 @@ export class RecorderInstaller {
execFileSync('codesign', ['--force', '--sign', '-', appBundlePath]);
console.log('VideoDB Capture: Code signed .app bundle');
} catch (e) {
console.warn('VideoDB Capture: codesign failed, screen recording may not work on macOS 26+');
console.warn(
'VideoDB Capture: codesign failed, screen recording may not work on macOS 26+'
);
}
}
}
Expand Down Expand Up @@ -353,7 +358,7 @@ export class RecorderInstaller {
}

return new Promise(resolve => {
const { spawn } = require('child_process');
const { spawn } = require('node:child_process');
const binaryPath = this.getBinaryPath();

const proc = spawn(binaryPath, ['--version']);
Expand Down
37 changes: 28 additions & 9 deletions src/core/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import type {
ListCaptureSessionsConfig,
CreateCaptureSessionConfig,
} from '@/types/capture';
import { HttpClient, type HttpClientAuthConfig } from '@/utils/httpClient';
import {
HttpClient,
type HttpClientAuthConfig,
type HttpClientOptions,
} from '@/utils/httpClient';
import { uploadToServer } from '@/utils/upload';

const {
Expand All @@ -40,13 +44,18 @@ const {
} = ApiPath;

class VdbHttpClient extends HttpClient {
constructor(baseURL: string, authConfig: HttpClientAuthConfig);
constructor(baseURL: string, apiKey: string);
constructor(
baseURL: string,
authConfigOrApiKey: HttpClientAuthConfig | string
authConfig: HttpClientAuthConfig,
options?: HttpClientOptions
);
constructor(baseURL: string, apiKey: string, options?: HttpClientOptions);
constructor(
baseURL: string,
authConfigOrApiKey: HttpClientAuthConfig | string,
options?: HttpClientOptions
) {
super(baseURL, authConfigOrApiKey as HttpClientAuthConfig);
super(baseURL, authConfigOrApiKey as HttpClientAuthConfig, options);
}
}

Expand All @@ -63,22 +72,32 @@ export class Connection {
* Create a connection with auth configuration
* @param baseURL - Base URL for the API
* @param authConfig - Authentication configuration (apiKey or sessionToken)
* @param options - Optional client configuration (custom headers).
* `headers` are auto-formatted to `x-kebab-case`, mirroring
* the kwarg-to-header behavior of `videodb-python`.
*/
constructor(baseURL: string, authConfig: HttpClientAuthConfig);
constructor(
baseURL: string,
authConfig: HttpClientAuthConfig,
options?: HttpClientOptions
);
/**
* Create a connection with API key (legacy signature)
* @param baseURL - Base URL for the API
* @param apiKey - API key for authentication
* @param options - Optional client configuration (custom headers)
* @deprecated Use the object-based constructor instead
*/
constructor(baseURL: string, apiKey: string);
constructor(baseURL: string, apiKey: string, options?: HttpClientOptions);
constructor(
baseURL: string,
authConfigOrApiKey: HttpClientAuthConfig | string
authConfigOrApiKey: HttpClientAuthConfig | string,
options?: HttpClientOptions
) {
this.vhttp = new VdbHttpClient(
baseURL,
authConfigOrApiKey as HttpClientAuthConfig
authConfigOrApiKey as HttpClientAuthConfig,
options
);
}

Expand Down
8 changes: 5 additions & 3 deletions src/core/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import {
NoDataResponse,
} from '@/types/response';
import type { Timeline, Transcript } from '@/types/video';
import { playStream, SnakeKeysToCamelCase } from '@/utils';
import { fromCamelToSnake, playStream, SnakeKeysToCamelCase } from '@/utils';
import { HttpClient } from '@/utils/httpClient';
import {
DefaultIndexType,
DefaultSearchType,
IndexTypeValues,
SceneExtractionType,
SubtitleStyleDefaultValues,
} from '@/core/config';
import { SearchFactory } from './search';
import { SearchResult } from './search/searchResult';
Expand Down Expand Up @@ -635,12 +636,13 @@ export class Video implements IVideo {
*
*/
public addSubtitle = async (config?: Partial<SubtitleStyleProps>) => {
const merged: SubtitleStyleProps = { ...SubtitleStyleDefaultValues, ...config };
const res = await this.#vhttp.post<
GenerateStreamResponse,
{ type: string; subtitle_style: Partial<SubtitleStyleProps> }
{ type: string; subtitle_style: Record<string, unknown> }
>([video, this.id, workflow], {
type: Workflows.addSubtitles,
subtitle_style: { ...config },
subtitle_style: fromCamelToSnake(merged),
});
return res.data.streamUrl;
};
Expand Down
14 changes: 9 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function connect(
return new Connection(baseURL, { apiKey });
}

// Handle new signature: connect({ apiKey?, sessionToken?, baseUrl? })
// Handle new signature: connect({ apiKey?, sessionToken?, baseUrl?, headers? })
const config = configOrApiKey;
const url = config.baseUrl || VIDEO_DB_API;

Expand All @@ -53,10 +53,14 @@ function connect(
);
}

return new Connection(url, {
apiKey: config.apiKey,
sessionToken: config.sessionToken,
});
return new Connection(
url,
{
apiKey: config.apiKey,
sessionToken: config.sessionToken,
},
config.headers ? { headers: config.headers } : undefined
);
}

export { Collection } from './core/collection';
Expand Down
6 changes: 6 additions & 0 deletions src/types/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface ConnectionConfig {
sessionToken?: string;
/** Base URL for the API (optional, defaults to https://api.videodb.io) */
baseUrl?: string;
/**
* Extra headers to attach to every request. Keys are auto-formatted to
* `x-kebab-case` (e.g. `org_id` → `x-org-id`), mirroring `videodb-python`'s
* kwarg-to-header behavior.
*/
headers?: Record<string, string>;
}

/**
Expand Down
44 changes: 41 additions & 3 deletions src/utils/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ export interface HttpClientAuthConfig {
sessionToken?: string;
}

/**
* Optional configuration for HttpClient
*
* `headers` is a free-form bag of extra headers merged into every
* request. Keys are auto-formatted to `x-kebab-case` to mirror
* `videodb-python`'s kwarg-to-header behavior, e.g. `org_id` → `x-org-id`.
*/
export interface HttpClientOptions {
headers?: Record<string, string>;
}

const formatExtraHeaders = (
headers?: Record<string, string>
): Record<string, string> => {
if (!headers) return {};
const formatted: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
const normalized = key.toLowerCase().replace(/_/g, '-');
const headerKey = normalized.startsWith('x-')
? normalized
: `x-${normalized}`;
formatted[headerKey] = value;
}
return formatted;
};

/**
* Api initialization to make axios config
* options available to all child classes
Expand All @@ -55,18 +81,29 @@ export class HttpClient {
* Create an HttpClient with auth configuration
* @param baseURL - Base URL for the API
* @param authConfig - Authentication configuration (apiKey or sessionToken)
* @param options - Optional client configuration (custom headers)
*/
protected constructor(baseURL: string, authConfig: HttpClientAuthConfig);
protected constructor(
baseURL: string,
authConfig: HttpClientAuthConfig,
options?: HttpClientOptions
);
/**
* Create an HttpClient with API key (legacy signature)
* @param baseURL - Base URL for the API
* @param apiKey - API key for authentication
* @param options - Optional client configuration (custom headers)
* @deprecated Use the object-based constructor instead
*/
protected constructor(baseURL: string, apiKey: string);
protected constructor(
baseURL: string,
authConfigOrApiKey: HttpClientAuthConfig | string
apiKey: string,
options?: HttpClientOptions
);
protected constructor(
baseURL: string,
authConfigOrApiKey: HttpClientAuthConfig | string,
options?: HttpClientOptions
) {
// Handle both signatures
const authConfig: HttpClientAuthConfig =
Expand All @@ -83,6 +120,7 @@ export class HttpClient {
headers: {
'x-access-token': token || '',
'x-videodb-client': SDK_CLIENT_HEADER,
...formatExtraHeaders(options?.headers),
},
timeout: HttpClientDefaultValues.timeout,
});
Expand Down
7 changes: 4 additions & 3 deletions src/utils/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Audio } from '@/core/audio';
import { Image } from '@/core/image';
import type { AudioBase, ImageBase, VideoBase } from '@/interfaces/core';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import { parse } from 'path';
import { createReadStream } from 'node:fs';
import { parse } from 'node:path';
import { HttpClient } from './httpClient';

const { upload_url, collection, upload } = ApiPath;
Expand Down Expand Up @@ -62,7 +62,8 @@ export const uploadToServer = async (
urlToUpload = data.url;
}

const name = data.name || ('filePath' in data ? parse(data.filePath).name : undefined);
const name =
data.name || ('filePath' in data ? parse(data.filePath).name : undefined);

const finalData = {
url: urlToUpload,
Expand Down
Loading