Skip to content

Commit

Permalink
Merge pull request #1160 from League-of-Foundry-Developers/0.8.x/av
Browse files Browse the repository at this point in the history
Update AV related classes
  • Loading branch information
UFOMelkor authored Aug 23, 2021
2 parents 6c91ee3 + 5c3c820 commit d480bdd
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 18 deletions.
43 changes: 38 additions & 5 deletions src/foundry/foundry.js/avClient.d.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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.
Expand All @@ -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<Record<string, string>>;
getAudioSinks(): Promise<Record<string, string>>;

/**
* 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<Record<string, string>>;
getAudioSources(): Promise<Record<string, string>>;

/**
* 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<Record<string, string>>;
getVideoSources(): Promise<Record<string, string>>;

/**
* 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<Record<string, string>>;

/**
* Return an array of Foundry User IDs which are currently connected to A/V.
Expand All @@ -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?
Expand Down
8 changes: 7 additions & 1 deletion src/foundry/foundry.js/avClients/easyRTCClient.d.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/foundry/foundry.js/avClients/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './easyRTCClient';
import './simplePeerAVClient';
138 changes: 138 additions & 0 deletions src/foundry/foundry.js/avClients/simplePeerAVClient.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, SimplePeer.Instance>;

/**
* A mapping of connected remote streams
*/
remoteStreams: Map<string, MediaStream>;

/**
* 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<boolean>;

/** @override */
disconnect(): Promise<boolean>;

/** @override */
initialize(): Promise<void>;

/** @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<void>;

/**
* Initialize a local media stream for the current user
*/
initializeLocalStream(): Promise<MediaStream | null>;

/**
* 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<SimplePeer.Instance>;

/**
* 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<void>;

/**
* Disconnect from all current peer streams
* @returns A Promise which resolves once all peers have been disconnected
*/
disconnectAll(): Promise<Array<void>>;

/** @override */
onSettingsChanged(changed: DeepPartial<AVSettings.Settings>): Promise<void>;

/**
* Replace the local stream for each connected peer with a re-generated MediaStream
*/
updateLocalStream(): Promise<void>;
}
2 changes: 1 addition & 1 deletion src/foundry/foundry.js/avMaster.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ declare class AVMaster {
/**
* The Audio/Video client class
*/
client: AVClient;
client: InstanceType<CONFIG['WebRTC']['clientClass']>;

/**
* A flag to track whether the current user is actively broadcasting their microphone.
Expand Down
16 changes: 10 additions & 6 deletions src/foundry/foundry.js/avSettings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
/**
Expand Down Expand Up @@ -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<typeof AVSettings.VOICE_MODES>;
interface DefaultVoiceModes {
ALWAYS: 'always';
ACTIVITY: 'activity';
PTT: 'ptt';
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Overrides {}
type VoiceModes = PropertyTypeOrFallback<AVSettings.Overrides, 'VoiceModes', DefaultVoiceModes>;
type VoiceMode = ValueOf<VoiceModes>;
}
5 changes: 4 additions & 1 deletion src/foundry/foundry.js/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1803,7 +1806,7 @@ declare global {
/**
* @defaultValue `SimplePeerAVClient`
*/
clientClass: ConstructorOf<AVClient>;
clientClass: PropertyTypeOrFallback<WebRTCConfig, 'clientClass', typeof AVClient>;

/**
* @defaultValue `50`
Expand Down
2 changes: 1 addition & 1 deletion src/foundry/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
16 changes: 13 additions & 3 deletions src/types/augments/simple-peer.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/types/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,5 @@ type StoredDocument<D extends foundry.abstract.Document<any, any>> = D & {
};

type TemporaryDocument<D> = D extends StoredDocument<infer U> ? U : D;

type PropertyTypeOrFallback<T, Key extends string, Fallback> = Key extends keyof T ? T[Key] : Fallback;
23 changes: 23 additions & 0 deletions test-d/foundry/avSettings.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<CustomVoiceModes>(AVSettings.VOICE_MODES);

const avMaster = new AVMaster();
expectType<'custom'>(avMaster.mode);
32 changes: 32 additions & 0 deletions test-d/foundry/foundry.js/avMaster.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expectType } from 'tsd';

declare class CustomAVCLient extends AVClient {
initialize(): Promise<void>;
connect(): Promise<boolean>;
disconnect(): Promise<boolean>;
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<void>;

customProperty: string;
}

declare global {
interface WebRTCConfig {
clientClass: typeof CustomAVCLient;
}
}

CONFIG.WebRTC.clientClass = CustomAVCLient;

const avMaster = new AVMaster();

expectType<string>(avMaster.client.customProperty);
if (game instanceof Game) {
expectType<string | undefined>(game?.webrtc?.client.customProperty);
}

0 comments on commit d480bdd

Please sign in to comment.