diff --git a/src/foundry/foundry.js/avClient.d.ts b/src/foundry/foundry.js/avClient.d.ts index 0c19374f4..452f85d35 100644 --- a/src/foundry/foundry.js/avClient.d.ts +++ b/src/foundry/foundry.js/avClient.d.ts @@ -1,5 +1,5 @@ /** - * An implementation interface for an Audio/Video client which is extended to provide broadcasting functionality. + * An interface for an Audio/Video client which is extended to provide broadcasting functionality. */ declare abstract class AVClient { /** @@ -8,10 +8,36 @@ declare abstract class AVClient { */ constructor(master: AVMaster, settings: AVSettings); + /** + * The master orchestration instance + */ master: AVMaster; + /** + * The active audio/video settings being used + */ settings: AVSettings; + /** + * Is audio broadcasting push-to-talk enabled? + */ + get isVoicePTT(): boolean; + + /** + * Is audio broadcasting always enabled? + */ + get isVoiceAlways(): boolean; + + /** + * Is audio broadcasting voice-activation enabled? + */ + get isVoiceActivated(): boolean; + + /** + * Is the current user muted? + */ + get isMuted(): boolean; + /** * One-time initialization actions that should be performed for this client implementation. * This will be called only once when the Game object is first set-up. @@ -37,19 +63,26 @@ declare abstract class AVClient { * Provide an Object of available audio sources which can be used by this implementation. * Each object key should be a device id and the key should be a human-readable label. */ - abstract getAudioSinks(): Promise>; + getAudioSinks(): Promise>; /** * Provide an Object of available audio sources which can be used by this implementation. * Each object key should be a device id and the key should be a human-readable label. */ - abstract getAudioSources(): Promise>; + getAudioSources(): Promise>; /** * Provide an Object of available video sources which can be used by this implementation. * Each object key should be a device id and the key should be a human-readable label. */ - abstract getVideoSources(): Promise>; + getVideoSources(): Promise>; + + /** + * Obtain a mapping of available device sources for a given type. + * @param kind - The type of device source being requested + * @internal + */ + _getSourcesOfType(kind: MediaDeviceKind): Promise>; /** * Return an array of Foundry User IDs which are currently connected to A/V. @@ -63,7 +96,7 @@ declare abstract class AVClient { * @param userId - The User id * @returns The MediaStream for the user, or null if the user does not have one */ - abstract getMediaStreamForUser(userId: string): MediaStream | null; + abstract getMediaStreamForUser(userId: string): MediaStream | null | undefined; /** * Is outbound audio enabled for the current user? diff --git a/src/foundry/foundry.js/avClients/easyRTCClient.d.ts b/src/foundry/foundry.js/avClients/easyRTCClient.d.ts index 478ba4a8f..3e252c364 100644 --- a/src/foundry/foundry.js/avClients/easyRTCClient.d.ts +++ b/src/foundry/foundry.js/avClients/easyRTCClient.d.ts @@ -1,6 +1,12 @@ /** * An AVClient implementation that uses WebRTC and the EasyRTC library. - */ + * This client is deprecated and will be removed entirely in 0.9.x. + * + * If you wish to continue using it, you will need to manually enable it by: + * 1. Include the easyrtc.js library which is no longer served + * 2. Set CONFIG.WebRTC.clientClass = EasyRTCClient + * + * @deprecated since 0.8.7 */ declare class EasyRTCClient extends AVClient { /** * @param master - The master orchestration instance diff --git a/src/foundry/foundry.js/avClients/index.d.ts b/src/foundry/foundry.js/avClients/index.d.ts new file mode 100644 index 000000000..b6f902d2e --- /dev/null +++ b/src/foundry/foundry.js/avClients/index.d.ts @@ -0,0 +1,2 @@ +import './easyRTCClient'; +import './simplePeerAVClient'; diff --git a/src/foundry/foundry.js/avClients/simplePeerAVClient.d.ts b/src/foundry/foundry.js/avClients/simplePeerAVClient.d.ts new file mode 100644 index 000000000..b7c5c55ee --- /dev/null +++ b/src/foundry/foundry.js/avClients/simplePeerAVClient.d.ts @@ -0,0 +1,138 @@ +/** + * An implementation of the AVClient which uses the simple-peer library and the Foundry socket server for signaling. + * Credit to bekit#4213 for identifying simple-peer as a viable technology and providing a POC implementation. + */ +declare class SimplePeerAVClient extends AVClient { + /** + * The local Stream which captures input video and audio + * @defaultValue `null` + */ + localStream: MediaStream | null; + + /** + * A mapping of connected peers + */ + peers: Map; + + /** + * A mapping of connected remote streams + */ + remoteStreams: Map; + + /** + * Has the client been successfully initialized? + * @defaultValue `false` + * @internal + */ + _initialized: boolean; + + /** + * Is outbound broadcast of local audio enabled? + * @defaultValue `false` + */ + audioBroadcastEnabled: boolean; + + /** @override */ + connect(): Promise; + + /** @override */ + disconnect(): Promise; + + /** @override */ + initialize(): Promise; + + /** @override */ + getConnectedUsers(): string[]; + + /** @override */ + getMediaStreamForUser(userId: string): MediaStream | null | undefined; + + /** @override */ + isAudioEnabled(): boolean; + + /** @override */ + isVideoEnabled(): boolean; + + /** @override */ + toggleAudio(enable: boolean): void; + + /** @override */ + toggleBroadcast(broadcast: boolean): void; + + /** @override */ + toggleVideo(enable: boolean): void; + + /** @override */ + setUserVideo(userId: string, videoElement: HTMLVideoElement): Promise; + + /** + * Initialize a local media stream for the current user + */ + initializeLocalStream(): Promise; + + /** + * Listen for Audio/Video updates on the av socket to broker connections between peers + */ + activateSocketListeners(): void; + + /** + * Initialize a stream connection with a new peer + * @param userId - The Foundry user ID for which the peer stream should be established + * @returns A Promise which resolves once the peer stream is initialized + */ + initializePeerStream(userId: string): Promise; + + /** + * Receive a request to establish a peer signal with some other User id + * @param userId - The Foundry user ID who is requesting to establish a connection + * @param data - The connection details provided by SimplePeer + */ + receiveSignal(userId: string, data: SimplePeer.SignalData): void; + + /** + * Connect to a peer directly, either as the initiator or as the receiver + * @param userId - The Foundry user ID with whom we are connecting + * @param isInitiator - Is the current user initiating the connection, or responding to it? + * (default: `false`) + * @returns The constructed and configured SimplePeer instance + */ + connectPeer(userId: string, isInitiator?: boolean): SimplePeer.Instance; + + /** + * Create the SimplePeer instance for the desired peer connection. + * Modules may implement more advanced connection strategies by overriding this method. + * @param userId - The Foundry user ID with whom we are connecting + * @param isInitiator - Is the current user initiating the connection, or responding to it? + * @internal + */ + _createPeerConnection(userId: string, isInitiator: boolean): SimplePeer.Instance; + + /** + * Setup the custom TURN relay to be used in subsequent calls if there is one configured. + * TURN credentials are mandatory in WebRTC. + * @param options - The SimplePeer configuration object. + * @internal + */ + _setupCustomTURN(options: SimplePeer.Options): void; + + /** + * Disconnect from a peer by stopping current stream tracks and destroying the SimplePeer instance + * @param userId - The Foundry user ID from whom we are disconnecting + * @returns A Promise which resolves once the disconnection is complete + */ + disconnectPeer(userId: string): Promise; + + /** + * Disconnect from all current peer streams + * @returns A Promise which resolves once all peers have been disconnected + */ + disconnectAll(): Promise>; + + /** @override */ + onSettingsChanged(changed: DeepPartial): Promise; + + /** + * Replace the local stream for each connected peer with a re-generated MediaStream + */ + updateLocalStream(): Promise; +} diff --git a/src/foundry/foundry.js/avMaster.d.ts b/src/foundry/foundry.js/avMaster.d.ts index 7c691f846..4e3a521f6 100644 --- a/src/foundry/foundry.js/avMaster.d.ts +++ b/src/foundry/foundry.js/avMaster.d.ts @@ -12,7 +12,7 @@ declare class AVMaster { /** * The Audio/Video client class */ - client: AVClient; + client: InstanceType; /** * A flag to track whether the current user is actively broadcasting their microphone. diff --git a/src/foundry/foundry.js/avSettings.d.ts b/src/foundry/foundry.js/avSettings.d.ts index 6937e4e05..bb24b7f34 100644 --- a/src/foundry/foundry.js/avSettings.d.ts +++ b/src/foundry/foundry.js/avSettings.d.ts @@ -21,11 +21,7 @@ declare class AVSettings { AUDIO_VIDEO: 3; }; - static VOICE_MODES: { - ALWAYS: 'always'; - ACTIVITY: 'activity'; - PTT: 'ptt'; - }; + static VOICE_MODES: AVSettings.VoiceModes; static DEFAULT_CLIENT_SETTINGS: { /** @@ -244,5 +240,13 @@ declare namespace AVSettings { type StoredUserSettings = typeof AVSettings.DEFAULT_USER_SETTINGS; type UserSettings = StoredUserSettings & { canBroadCastAudio: boolean; canBroadcastVideo: boolean }; type Settings = { client: ClientSettings; world: WorldSettings }; - type VoiceMode = ValueOf; + interface DefaultVoiceModes { + ALWAYS: 'always'; + ACTIVITY: 'activity'; + PTT: 'ptt'; + } + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Overrides {} + type VoiceModes = PropertyTypeOrFallback; + type VoiceMode = ValueOf; } diff --git a/src/foundry/foundry.js/config.d.ts b/src/foundry/foundry.js/config.d.ts index 9dfe98899..8b9f36ca1 100644 --- a/src/foundry/foundry.js/config.d.ts +++ b/src/foundry/foundry.js/config.d.ts @@ -125,6 +125,9 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-empty-interface interface FlagConfig {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface WebRTCConfig {} + /** * Runtime configuration settings for Foundry VTT which exposes a large number of variables which determine how * aspects of the software behaves. @@ -1803,7 +1806,7 @@ declare global { /** * @defaultValue `SimplePeerAVClient` */ - clientClass: ConstructorOf; + clientClass: PropertyTypeOrFallback; /** * @defaultValue `50` diff --git a/src/foundry/index.d.ts b/src/foundry/index.d.ts index ada2db88c..7d128cf56 100644 --- a/src/foundry/index.d.ts +++ b/src/foundry/index.d.ts @@ -53,7 +53,7 @@ import './foundry.js/videoHelper'; import './foundry.js/applications'; -import './foundry.js/avClients/easyRTCClient'; +import './foundry.js/avClients'; import './foundry.js/clientDocuments/activeEffect'; import './foundry.js/clientDocuments/actor'; diff --git a/src/types/augments/simple-peer.d.ts b/src/types/augments/simple-peer.d.ts index 3d07ec791..4b40e3140 100644 --- a/src/types/augments/simple-peer.d.ts +++ b/src/types/augments/simple-peer.d.ts @@ -1,3 +1,13 @@ -import * as SimplePeer from 'simple-peer'; -export = SimplePeer; -export as namespace SimplePeer; +import * as _SimplePeer from 'simple-peer'; + +declare global { + namespace SimplePeer { + type Options = _SimplePeer.Options; + type SimplePeer = _SimplePeer.SimplePeer; + type TypedArray = _SimplePeer.TypedArray; + type SimplePeerData = _SimplePeer.SimplePeerData; + type SignalData = _SimplePeer.SignalData; + type Instance = _SimplePeer.Instance; + } + const SimplePeer: SimplePeer.SimplePeer; +} diff --git a/src/types/utils.d.ts b/src/types/utils.d.ts index 9896093fb..9247edb3a 100644 --- a/src/types/utils.d.ts +++ b/src/types/utils.d.ts @@ -126,3 +126,5 @@ type StoredDocument> = D & { }; type TemporaryDocument = D extends StoredDocument ? U : D; + +type PropertyTypeOrFallback = Key extends keyof T ? T[Key] : Fallback; diff --git a/test-d/foundry/avSettings.test-d.ts b/test-d/foundry/avSettings.test-d.ts new file mode 100644 index 000000000..0a6be8b66 --- /dev/null +++ b/test-d/foundry/avSettings.test-d.ts @@ -0,0 +1,23 @@ +import { expectType } from 'tsd'; + +interface CustomVoiceModes { + SOME_CUSTOM_MODE: 'custom'; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace AVSettings { + interface Overrides { + VoiceModes: CustomVoiceModes; + } + } +} + +AVSettings.VOICE_MODES = { + SOME_CUSTOM_MODE: 'custom' +}; + +expectType(AVSettings.VOICE_MODES); + +const avMaster = new AVMaster(); +expectType<'custom'>(avMaster.mode); diff --git a/test-d/foundry/foundry.js/avMaster.test-d.ts b/test-d/foundry/foundry.js/avMaster.test-d.ts new file mode 100644 index 000000000..a603e423c --- /dev/null +++ b/test-d/foundry/foundry.js/avMaster.test-d.ts @@ -0,0 +1,32 @@ +import { expectType } from 'tsd'; + +declare class CustomAVCLient extends AVClient { + initialize(): Promise; + connect(): Promise; + disconnect(): Promise; + getConnectedUsers(): string[]; + getMediaStreamForUser(userId: string): MediaStream | null; + isAudioEnabled(): boolean; + isVideoEnabled(): boolean; + toggleAudio(enable: boolean): void; + toggleBroadcast(broadcast: boolean): void; + toggleVideo(enable: boolean): void; + setUserVideo(userId: string, videoElement: HTMLVideoElement): Promise; + + customProperty: string; +} + +declare global { + interface WebRTCConfig { + clientClass: typeof CustomAVCLient; + } +} + +CONFIG.WebRTC.clientClass = CustomAVCLient; + +const avMaster = new AVMaster(); + +expectType(avMaster.client.customProperty); +if (game instanceof Game) { + expectType(game?.webrtc?.client.customProperty); +}