diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcfe0e..db747b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,29 @@ # Changelog -## [0.2.1] (2026-03-10) +## [0.2.2] (2026-03-10) ### Added -- RTStream `generateStream(start, end, playerConfig?)` now supports `playerConfig.title`, `playerConfig.description`, and `playerConfig.slug` for player share metadata. +- RTStream `generateStream()` now returns `playerUrl` and stores both `streamUrl` and `playerUrl` +- RTStream `playerConfig` parameter with `title`, `description`, and `slug` for player share metadata +- CaptureSession `channels`, `primaryVideoChannelId`, and `exportStatus` properties +- CaptureSession `displays` getter for video channels +- CaptureSession `export()` method with optional `videoChannelId` and `wsConnectionId` + +### Changed + +- Updated capture binary to v0.2.8 with new checksums +- Binary distribution URL migrated to CloudFront (`https://artifacts.videodb.io/capture`) + +--- + +## [0.2.1] (2026-02-06) + +### Changed + +- Binary distribution URL migrated and checksum updates + +--- ## [0.2.0] (2026-02-02) diff --git a/lib/installer.js b/lib/installer.js index 8e575fc..c9a539e 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -2,8 +2,11 @@ const fs = require('fs'); const path = require('path'); const https = require('https'); const crypto = require('crypto'); +const { execFileSync } = require('child_process'); const tar = require('tar'); +const MACOS_APP_BUNDLE = 'VideoDBCapture.app'; + class Installer { constructor(config) { this.config = config; @@ -19,7 +22,7 @@ class Installer { getDownloadUrl() { const platformKey = `${this.platform}-${this.arch}`; - return `${this.config.baseUrl}/v${this.config.version}/recorder-${platformKey}.tar.gz`; + return `${this.config.baseUrl}/v${this.config.version}/capture-${platformKey}.tar.gz`; } getExpectedChecksum() { @@ -27,10 +30,24 @@ class Installer { return this.config.checksums[platformKey]; } + getBinaryPath() { + if (this.platform === 'darwin') { + return path.join(this.binDir, MACOS_APP_BUNDLE, 'Contents', 'MacOS', 'capture'); + } else if (this.platform === 'win32') { + return path.join(this.binDir, 'capture.exe'); + } + return path.join(this.binDir, 'capture'); + } + async downloadFile(url, destPath) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(destPath); - https.get(url, (response) => { + const options = { + headers: { + 'User-Agent': 'videodb-node-installer' + } + }; + https.get(url, options, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { file.close(); fs.unlinkSync(destPath); @@ -75,7 +92,7 @@ class Installer { } async install() { - console.log(`Installing VideoDB Recorder for ${this.platform}-${this.arch}...`); + console.log(`Installing VideoDB Capture for ${this.platform}-${this.arch}...`); if (!this.isPlatformSupported()) { const platformKey = `${this.platform}-${this.arch}`; @@ -90,9 +107,8 @@ class Installer { } const platformKey = `${this.platform}-${this.arch}`; - const tarPath = path.join(this.binDir, `recorder-${platformKey}.tar.gz`); - const binaryName = this.platform === 'win32' ? 'recorder.exe' : 'recorder'; - const binaryPath = path.join(this.binDir, binaryName); + const tarPath = path.join(this.binDir, `capture-${platformKey}.tar.gz`); + const binaryPath = this.getBinaryPath(); const url = this.getDownloadUrl(); if (fs.existsSync(binaryPath)) { @@ -116,10 +132,32 @@ class Installer { fs.unlinkSync(tarPath); + // Make binary executable (Unix) if (this.platform !== 'win32') { fs.chmodSync(binaryPath, 0o755); } + // Re-sign the .app bundle on macOS (extraction can invalidate signatures) + if (this.platform === 'darwin') { + const appBundlePath = path.join(this.binDir, MACOS_APP_BUNDLE); + if (fs.existsSync(appBundlePath)) { + try { + const macosDir = path.join(appBundlePath, 'Contents', 'MacOS'); + const dylibPath = path.join(macosDir, 'librecorder.dylib'); + const capturePath = path.join(macosDir, 'capture'); + + if (fs.existsSync(dylibPath)) { + execFileSync('codesign', ['--force', '--sign', '-', dylibPath]); + } + execFileSync('codesign', ['--force', '--sign', '-', capturePath]); + execFileSync('codesign', ['--force', '--sign', '-', appBundlePath]); + console.log('Code signed .app bundle.'); + } catch (e) { + console.warn('codesign failed, screen recording may not work on macOS 26+'); + } + } + } + console.log('Installation complete!'); } catch (error) { console.error('Installation failed:', error.message); diff --git a/package-lock.json b/package-lock.json index 4b6eb8b..e5d9457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1456,7 +1455,6 @@ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1551,7 +1549,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -1721,7 +1718,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2085,7 +2081,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2655,7 +2650,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2712,7 +2706,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3846,7 +3839,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5145,7 +5137,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5861,7 +5852,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5989,7 +5979,6 @@ "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", @@ -6025,7 +6014,6 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 24bfe60..c4f1392 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "videodb", - "version": "0.2.1", + "version": "0.2.2", "description": "A NodeJS wrapper for VideoDB's API written in TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -10,8 +10,8 @@ "default": "./dist/index.js" }, "./capture": { - "types": "./dist/recorder/index.d.ts", - "default": "./dist/recorder/index.js" + "types": "./dist/capture/index.d.ts", + "default": "./dist/capture/index.js" } }, "files": [ @@ -39,12 +39,12 @@ "prepublishOnly": "npm run build" }, "binaryConfig": { - "baseUrl": "https://recorder-sdk-binaries.s3.amazonaws.com", - "version": "0.2.7", + "baseUrl": "https://artifacts.videodb.io/capture", + "version": "0.2.8", "checksums": { - "darwin-arm64": "3592c47331a5dd68b3d94474c307cf7da5bb182aa9600ba65d2c4257244e7670", - "darwin-x64": "2f66e228cbab0195090cc3628ceaf75d298e7f8c2269bac6446b6ba23fb778c2", - "win32-x64": "23ae68bfe8e017b1da960e6b33a258f47b7307c9005506a59f3af0802799d057" + "darwin-arm64": "4aab67e524c2541bebbae24b8dd845da5d7f74fba006ce860a4914844e185c5d", + "darwin-x64": "ac67dc1a9edd2094d36e6961ed1dabab3e8b3e3e2a61655a49195b7e518901ca", + "win32-x64": "f19110d9b632c0149088abc09f4c86e0f43f64ce4b52a73bca6eb20789e156d0" } }, "repository": { diff --git a/src/recorder/binaryManager.ts b/src/capture/binaryManager.ts similarity index 87% rename from src/recorder/binaryManager.ts rename to src/capture/binaryManager.ts index de416b1..5612b7f 100644 --- a/src/recorder/binaryManager.ts +++ b/src/capture/binaryManager.ts @@ -20,7 +20,7 @@ interface PendingCommand { } /** - * BinaryManager handles communication with the native recorder binary + * BinaryManager handles communication with the native capture binary * Manages the child process lifecycle, health checks, and auto-restart */ export class BinaryManager extends EventEmitter { @@ -119,16 +119,16 @@ export class BinaryManager extends EventEmitter { const duration = this.lastHealthCheck - start; if (duration > 5000) { - console.warn(`VideoDB Recorder: Health check slow (${duration}ms)`); + console.warn(`VideoDB Capture: Health check slow (${duration}ms)`); } } catch { - console.error('VideoDB Recorder: Health check failed'); + console.error('VideoDB Capture: Health check failed'); if ( this.lastHealthCheck && Date.now() - this.lastHealthCheck > HEALTH_CHECK_TIMEOUT ) { - console.error('VideoDB Recorder: Binary appears hung, restarting...'); + console.error('VideoDB Capture: Binary appears hung, restarting...'); this.emit('error', { type: 'health_check_timeout', message: 'Binary not responding to health checks', @@ -157,7 +157,7 @@ export class BinaryManager extends EventEmitter { */ public async start(config: Record = {}): Promise { if (this.process && this.process.exitCode === null) { - console.warn('VideoDB Recorder: Process already running'); + console.warn('VideoDB Capture: Process already running'); return; } @@ -165,7 +165,7 @@ export class BinaryManager extends EventEmitter { // Ensure binary is installed if (!this.isDev && !this.installer.isInstalled()) { - console.log('VideoDB Recorder: Installing binary...'); + console.log('VideoDB Capture: Installing binary...'); await this.installer.install(); } @@ -178,7 +178,7 @@ export class BinaryManager extends EventEmitter { // Handle stdin errors (e.g. EPIPE when binary exits unexpectedly) this.process.stdin!.on('error', (err: Error) => { - console.error(`VideoDB Recorder: stdin write error: ${err.message}`); + console.error(`VideoDB Capture: stdin write error: ${err.message}`); }); // Handle stderr (logs and errors) @@ -189,7 +189,7 @@ export class BinaryManager extends EventEmitter { stderrRl.on('line', (line: string) => { this.appendError(line); - console.error(`[Recorder Binary]: ${line}`); + console.error(`[Capture Binary]: ${line}`); }); // Handle stdout (protocol messages) @@ -209,25 +209,25 @@ export class BinaryManager extends EventEmitter { } } else { // Non-protocol output (debug logs from binary) - console.log(`[Recorder Binary Debug]: ${line}`); + console.log(`[Capture Binary Debug]: ${line}`); } }); // Handle process errors this.process.on('error', (error: Error) => { - console.error(`VideoDB Recorder: Process error: ${error.message}`); + console.error(`VideoDB Capture: Process error: ${error.message}`); this.flushPendingCommands(new Error(`Process error: ${error.message}`)); this.emit('error', { type: 'process', message: - 'The recorder binary process failed to start or exited improperly.', + 'The capture binary process failed to start or exited improperly.', }); }); // Handle process exit this.process.on('exit', (code: number | null, signal: string | null) => { console.log( - `VideoDB Recorder: Process exited with code ${code}, signal ${signal}` + `VideoDB Capture: Process exited with code ${code}, signal ${signal}` ); this.flushPendingCommands(new Error(`Process exited: ${code}`)); @@ -244,7 +244,7 @@ export class BinaryManager extends EventEmitter { if (this.restartOnError && this.remainingRestarts > 0) { this.remainingRestarts--; console.warn( - `VideoDB Recorder: Auto-restarting (${this.remainingRestarts} restarts remaining)` + `VideoDB Capture: Auto-restarting (${this.remainingRestarts} restarts remaining)` ); console.warn( `Last errors:\n${this.errorBuffer.slice(-10).join('\n')}` @@ -255,7 +255,7 @@ export class BinaryManager extends EventEmitter { }, 1000); } else if (this.remainingRestarts === 0) { console.error( - 'VideoDB Recorder: Max restart attempts reached. Manual intervention required' + 'VideoDB Capture: Max restart attempts reached. Manual intervention required' ); this.emit('error', { type: 'max_restarts', @@ -266,7 +266,7 @@ export class BinaryManager extends EventEmitter { }); if (this.unexpectedShutdown) { - console.log('VideoDB Recorder: Recovered from unexpected shutdown'); + console.log('VideoDB Capture: Recovered from unexpected shutdown'); this.unexpectedShutdown = false; } @@ -276,7 +276,7 @@ export class BinaryManager extends EventEmitter { // Send init command await this.sendCommand('init', config); } catch (error) { - console.error('VideoDB Recorder: Failed to initialize:', error); + console.error('VideoDB Capture: Failed to initialize:', error); throw error; } } @@ -332,13 +332,13 @@ export class BinaryManager extends EventEmitter { const currentProc = this.process; setTimeout(() => { if (currentProc && !currentProc.killed) { - console.warn('VideoDB Recorder: Force killing process after timeout'); + console.warn('VideoDB Capture: Force killing process after timeout'); currentProc.kill(); } }, 5000); } catch { // If shutdown command fails, just kill - console.warn('VideoDB Recorder: Shutdown command failed, force killing'); + console.warn('VideoDB Capture: Shutdown command failed, force killing'); if (this.process) { this.process.kill(); } diff --git a/src/recorder/captureClient.ts b/src/capture/captureClient.ts similarity index 99% rename from src/recorder/captureClient.ts rename to src/capture/captureClient.ts index f2dede5..e8160a5 100644 --- a/src/recorder/captureClient.ts +++ b/src/capture/captureClient.ts @@ -153,7 +153,7 @@ export class CaptureClient extends EventEmitter implements ChannelClient { if (!status) { console.warn( - 'VideoDB Recorder: Unexpected requestPermission response:', + 'VideoDB Capture: Unexpected requestPermission response:', result ); return PermissionStatus.undetermined; diff --git a/src/recorder/channel.ts b/src/capture/channel.ts similarity index 100% rename from src/recorder/channel.ts rename to src/capture/channel.ts diff --git a/src/recorder/index.ts b/src/capture/index.ts similarity index 95% rename from src/recorder/index.ts rename to src/capture/index.ts index 1906c65..2010c5f 100644 --- a/src/recorder/index.ts +++ b/src/capture/index.ts @@ -1,7 +1,7 @@ /** - * VideoDB Recorder Module + * VideoDB Capture Module * - * This module provides client-side recording capabilities using a native binary. + * This module provides client-side capture capabilities using a native binary. * It supports capturing audio (microphone, system audio) and video (screen capture). * * @example diff --git a/src/recorder/installer.ts b/src/capture/installer.ts similarity index 68% rename from src/recorder/installer.ts rename to src/capture/installer.ts index d503900..2169321 100644 --- a/src/recorder/installer.ts +++ b/src/capture/installer.ts @@ -2,8 +2,12 @@ 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 type { BinaryConfig, PlatformInfo } from './types'; +/** Name of the macOS .app bundle that wraps the capture binary for TCC compatibility */ +const MACOS_APP_BUNDLE = 'VideoDBCapture.app'; + /** Supported platforms */ const SUPPORTED_PLATFORMS: Record = { darwin: ['x64', 'arm64'], @@ -32,12 +36,12 @@ export class RecorderInstaller { constructor(binaryConfig?: BinaryConfig) { // Default binary config - can be overridden or loaded from package.json this.binaryConfig = binaryConfig || { - baseUrl: 'https://recorder-sdk-binaries.s3.amazonaws.com', - version: '0.2.7', + baseUrl: 'https://artifacts.videodb.io/capture', + version: '0.2.8', checksums: { - 'darwin-x64': '8ebea82d67d5da55d387fb1e0a58076016932b00201b9452fa1c8f44a2eaf533', - 'darwin-arm64': '79c6671a020bd2c7ecbd258bb41d3453b57b93ec5d6be3acd17e6c581f3a47c5', - 'win32-x64': '34d5111877b58e0ad633faf993fa872ab8ef27df45fec491836c09b29fd361ef', + 'darwin-x64': 'ac67dc1a9edd2094d36e6961ed1dabab3e8b3e3e2a61655a49195b7e518901ca', + 'darwin-arm64': '4aab67e524c2541bebbae24b8dd845da5d7f74fba006ce860a4914844e185c5d', + 'win32-x64': 'f19110d9b632c0149088abc09f4c86e0f43f64ce4b52a73bca6eb20789e156d0', }, }; @@ -70,21 +74,39 @@ export class RecorderInstaller { public getDownloadUrl(): string { const { platformKey } = this.getPlatformInfo(); const { baseUrl, version } = this.binaryConfig; - return `${baseUrl}/v${version}/recorder-${platformKey}.tar.gz`; + return `${baseUrl}/v${version}/capture-${platformKey}.tar.gz`; } /** - * Get the path where the binary should be installed + * Get the path where the binary should be installed. + * + * On macOS the binary lives inside a .app bundle so that TCC + * (macOS 26+) can grant screen-recording permissions. */ public getBinaryPath(): string { - const binName = process.platform === 'win32' ? 'recorder.exe' : 'recorder'; + let binPath: string; + + if (process.platform === 'darwin') { + // macOS: binary is inside the .app bundle + binPath = path.join( + this.binDir, + MACOS_APP_BUNDLE, + 'Contents', + 'MacOS', + 'capture', + ); + } else if (process.platform === 'win32') { + binPath = path.join(this.binDir, 'capture.exe'); + } else { + binPath = path.join(this.binDir, 'capture'); + } // Try multiple paths in order of priority const possiblePaths = [ // 1. Electron unpacked (highest priority) - path.join(this.binDir, binName).replace('app.asar', 'app.asar.unpacked'), + binPath.replace('app.asar', 'app.asar.unpacked'), // 2. Standard location - path.join(this.binDir, binName), + binPath, ]; // Find the first path that exists @@ -129,7 +151,13 @@ export class RecorderInstaller { return new Promise((resolve, reject) => { const file = fs.createWriteStream(destPath); - const request = https.get(url, response => { + const options = { + headers: { + 'User-Agent': 'videodb-node-installer', + }, + }; + + const request = https.get(url, options, response => { // Handle redirects if ( response.statusCode && @@ -205,7 +233,7 @@ export class RecorderInstaller { // Check if already installed if (!force && this.isInstalled()) { - console.log('VideoDB Recorder: Binary already installed'); + console.log('VideoDB Capture: Binary already installed'); return; } @@ -213,7 +241,7 @@ export class RecorderInstaller { if (!this.isPlatformSupported()) { const { platformKey } = this.getPlatformInfo(); throw new Error( - `VideoDB Recorder: Platform ${platformKey} is not supported. ` + + `VideoDB Capture: Platform ${platformKey} is not supported. ` + `Supported platforms: ${Object.entries(SUPPORTED_PLATFORMS) .map(([p, archs]) => archs.map(a => `${p}-${a}`).join(', ')) .join(', ')}` @@ -226,9 +254,9 @@ export class RecorderInstaller { } const downloadUrl = this.getDownloadUrl(); - const archivePath = path.join(this.binDir, 'recorder.tar.gz'); + const archivePath = path.join(this.binDir, 'capture.tar.gz'); - console.log(`VideoDB Recorder: Downloading from ${downloadUrl}...`); + console.log(`VideoDB Capture: Downloading from ${downloadUrl}...`); try { // Download the archive @@ -244,12 +272,12 @@ export class RecorderInstaller { `Checksum mismatch: expected ${expectedChecksum}, got ${actualChecksum}` ); } - console.log('VideoDB Recorder: Checksum verified'); + console.log('VideoDB Capture: Checksum verified'); } } // Extract the archive - console.log('VideoDB Recorder: Extracting...'); + console.log('VideoDB Capture: Extracting...'); await this.extractTarGz(archivePath, this.binDir); // Make binary executable (Unix) @@ -258,10 +286,34 @@ export class RecorderInstaller { fs.chmodSync(binaryPath, 0o755); } + // Re-sign the .app bundle on macOS so TCC recognises it. + // The bundle ships pre-signed, but extraction can invalidate + // the signature on some systems. Sign inside-out: dylib, then + // binary, then the bundle itself. + if (process.platform === 'darwin') { + const appBundlePath = path.join(this.binDir, MACOS_APP_BUNDLE); + if (fs.existsSync(appBundlePath)) { + try { + const macosDir = path.join(appBundlePath, 'Contents', 'MacOS'); + const dylibPath = path.join(macosDir, 'librecorder.dylib'); + const capturePath = path.join(macosDir, 'capture'); + + if (fs.existsSync(dylibPath)) { + execFileSync('codesign', ['--force', '--sign', '-', dylibPath]); + } + execFileSync('codesign', ['--force', '--sign', '-', capturePath]); + 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+'); + } + } + } + // Clean up archive fs.unlinkSync(archivePath); - console.log('VideoDB Recorder: Installation complete'); + console.log('VideoDB Capture: Installation complete'); } catch (error) { // Clean up on error if (fs.existsSync(archivePath)) { @@ -275,15 +327,25 @@ export class RecorderInstaller { * Uninstall the binary */ public uninstall(): void { + if (process.platform === 'darwin') { + // Remove the entire .app bundle directory + const appBundlePath = path.join(this.binDir, MACOS_APP_BUNDLE); + if (fs.existsSync(appBundlePath)) { + fs.rmSync(appBundlePath, { recursive: true }); + console.log('VideoDB Capture: App bundle uninstalled'); + return; + } + } + const binaryPath = this.getBinaryPath(); if (fs.existsSync(binaryPath)) { fs.unlinkSync(binaryPath); - console.log('VideoDB Recorder: Binary uninstalled'); + console.log('VideoDB Capture: Binary uninstalled'); } } /** - * Get the binary version (by running recorder --version) + * Get the binary version (by running capture --version) */ public async getInstalledVersion(): Promise { if (!this.isInstalled()) { diff --git a/src/recorder/types.ts b/src/capture/types.ts similarity index 99% rename from src/recorder/types.ts rename to src/capture/types.ts index 7d86a43..fc62dd9 100644 --- a/src/recorder/types.ts +++ b/src/capture/types.ts @@ -36,7 +36,7 @@ export interface PlatformInfo { } /** - * Binary channel from native recorder + * Binary channel from native capture */ export interface BinaryChannel { /** Channel identifier (e.g., 'mic:default', 'display:1') */ diff --git a/src/core/captureSession.ts b/src/core/captureSession.ts index fee7d33..a625f55 100644 --- a/src/core/captureSession.ts +++ b/src/core/captureSession.ts @@ -1,6 +1,10 @@ import { ApiPath } from '@/constants'; import { RTStream } from '@/core/rtstream'; -import type { CaptureSessionBase, RTStreamBase } from '@/interfaces/core'; +import type { + CaptureSessionBase, + CaptureSessionChannelBase, + RTStreamBase, +} from '@/interfaces/core'; import type { CaptureSessionStatusType } from '@/types/capture'; import { HttpClient } from '@/utils/httpClient'; @@ -38,6 +42,9 @@ export class CaptureSession { public exportedVideoId?: string; public rtstreams: RTStream[]; public createdAt?: number; + public channels: CaptureSessionChannelBase[]; + public primaryVideoChannelId?: string; + public exportStatus?: string; #vhttp: HttpClient; constructor(http: HttpClient, data: CaptureSessionBase) { @@ -51,6 +58,9 @@ export class CaptureSession { this.metadata = data.metadata; this.exportedVideoId = data.exportedVideoId; this.createdAt = data.createdAt; + this.channels = data.channels || []; + this.primaryVideoChannelId = data.primaryVideoChannelId; + this.exportStatus = data.exportStatus; this.rtstreams = []; } @@ -85,6 +95,9 @@ export class CaptureSession { this.metadata = data.metadata; this.exportedVideoId = data.exportedVideoId; this.createdAt = data.createdAt; + this.channels = data.channels || []; + this.primaryVideoChannelId = data.primaryVideoChannelId; + this.exportStatus = data.exportStatus; // Build RTStream instances from the response // API returns: { rtstream_id, channel_id, status } → { rtstreamId, channelId, status } @@ -136,6 +149,47 @@ export class CaptureSession { return filtered; }; + /** + * Get video channels in the session + * @returns Array of video channel objects + */ + public get displays(): CaptureSessionChannelBase[] { + return this.channels.filter(ch => ch.type === 'video'); + } + + /** + * Trigger export for this capture session. + * + * Call repeatedly to poll for completion. Returns exportStatus + * of "exporting" while in progress, "exported" with videoId, + * streamUrl, and playerUrl when done. + * + * @param videoChannelId - Optional channel ID of the video to export. Defaults to the primary video channel. + * @param wsConnectionId - WebSocket connection ID for push notification when export completes (optional). + * @returns Export response + */ + public export = async ( + videoChannelId?: string, + wsConnectionId?: string + ): Promise> => { + const data: Record = {}; + if (videoChannelId) data.video_channel_id = videoChannelId; + if (wsConnectionId) data.connection_id = wsConnectionId; + + const res = await this.#vhttp.post, typeof data>( + [ + ApiPath.collection, + this.collectionId, + ApiPath.capture, + ApiPath.session, + this.id, + ApiPath.export, + ], + data + ); + return res.data || {}; + }; + /** * String representation of the CaptureSession */ diff --git a/src/core/rtstream.ts b/src/core/rtstream.ts index 2a5dcbb..97d1d10 100644 --- a/src/core/rtstream.ts +++ b/src/core/rtstream.ts @@ -349,11 +349,14 @@ export class RTStream { public createdAt?: string; public sampleRate?: number; public status?: string; - public playerUrl?: string; /** Channel ID this rtstream is associated with */ public channelId?: string; /** Media types this rtstream handles */ public mediaTypes?: string[]; + /** Generated playback URL for the rtstream segment */ + public streamUrl?: string; + /** Player URL for the generated rtstream segment */ + public playerUrl?: string; #vhttp: HttpClient; constructor(http: HttpClient, data: RTStreamBase) { @@ -426,7 +429,7 @@ export class RTStream { * @param start - Start time of the stream in Unix timestamp format * @param end - End time of the stream in Unix timestamp format * @param playerConfig - Optional player share page metadata - * @returns Stream URL + * @returns Player URL */ public generateStream = async ( start: number, @@ -445,12 +448,13 @@ export class RTStream { params.player_slug_prefix = playerConfig.slug; } - const res = await this.#vhttp.get<{ streamUrl: string; playerUrl?: string }>( - [ApiPath.rtstream, this.id, ApiPath.stream], - { params } - ); + const res = await this.#vhttp.get<{ + streamUrl: string; + playerUrl: string; + }>([ApiPath.rtstream, this.id, ApiPath.stream], { params }); + this.streamUrl = res.data?.streamUrl; this.playerUrl = res.data?.playerUrl; - return res.data?.streamUrl || null; + return this.playerUrl || null; }; /** diff --git a/src/index.ts b/src/index.ts index 81d4159..9396948 100644 --- a/src/index.ts +++ b/src/index.ts @@ -232,4 +232,4 @@ export { type StartCaptureSessionClientConfig, type CaptureClientOptions, type TrackTypeValue, -} from './recorder'; +} from './capture'; diff --git a/src/interfaces/core.ts b/src/interfaces/core.ts index d7d87dc..6f3dec3 100644 --- a/src/interfaces/core.ts +++ b/src/interfaces/core.ts @@ -311,6 +311,14 @@ export interface RecordMeetingConfig { /** * Base type for CaptureSession objects */ +export interface CaptureSessionChannelBase { + channelId: string; + name: string; + type: 'video' | 'audio'; + isPrimary?: boolean; + store?: boolean; +} + export interface CaptureSessionBase { id: string; collectionId: string; @@ -321,6 +329,9 @@ export interface CaptureSessionBase { metadata?: Record; exportedVideoId?: string; createdAt?: number; + channels?: CaptureSessionChannelBase[]; + primaryVideoChannelId?: string; + exportStatus?: string; } /**