@simplito/privmx-webendpoint
Version:
PrivMX Web Endpoint library
437 lines (436 loc) • 19.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamApi = void 0;
const Utils_1 = require("../webStreams/Utils");
const BaseApi_1 = require("./BaseApi");
/**
* `StreamApi` is a class representing Endpoint's API for Stream Rooms.
*/
class StreamApi extends BaseApi_1.BaseApi {
native;
client;
constructor(native, ptr, client) {
super(ptr);
this.native = native;
this.client = client;
}
// local data
streams = new Map();
streamTracks = new Map();
/**
* Creates a new Stream Room in given Context.
*
* @param {string} contextId ID of the Context to create the Stream Room in
* @param {UserWithPubKey[]} users array of UserWithPubKey structs which indicates who will have access to the created Stream Room
* @param {UserWithPubKey[]} managers array of UserWithPubKey structs which indicates who will have access (and management rights) to the created Stream Room
* @param {Uint8Array} publicMeta public (unencrypted) metadata
* @param {Uint8Array} privateMeta private (encrypted) metadata
* @param {ContainerPolicy} policies Stream Room's policies (pass `undefined` to use defaults)
* @returns {string} ID of the created Stream Room
*/
async createStreamRoom(contextId, users, managers, publicMeta, privateMeta, policies) {
const res = await this.native.createStreamRoom(this.servicePtr, [
contextId,
users,
managers,
publicMeta,
privateMeta,
policies,
]);
return res;
}
/**
* Updates an existing Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room to update
* @param {UserWithPubKey[]} users array of UserWithPubKey structs which indicates who will have access to the Stream Room
* @param {UserWithPubKey[]} managers array of UserWithPubKey structs which indicates who will have access (and management rights) to the Stream Room
* @param {Uint8Array} publicMeta public (unencrypted) metadata
* @param {Uint8Array} privateMeta private (encrypted) metadata
* @param {number} version current version of the updated Stream Room
* @param {boolean} force force update (without checking version)
* @param {boolean} forceGenerateNewKey force to regenerate a key for the Stream Room
* @param {ContainerPolicy} policies Stream Room's policies (pass `undefined` to keep current/defaults)
*/
async updateStreamRoom(streamRoomId, users, managers, publicMeta, privateMeta, version, force, forceGenerateNewKey, policies) {
return this.native.updateStreamRoom(this.servicePtr, [
streamRoomId,
users,
managers,
publicMeta,
privateMeta,
version,
force,
forceGenerateNewKey,
policies,
]);
}
/**
* Gets a list of Stream Rooms in given Context.
*
* @param {string} contextId ID of the Context to get the Stream Rooms from
* @param {PagingQuery} query struct with list query parameters
* @returns {PagingList<StreamRoom>} list of Stream Rooms
*/
async listStreamRooms(contextId, query) {
return this.native.listStreamRooms(this.servicePtr, [contextId, query]);
}
/**
* Joins a Stream Room.
*
* This is required before calling `createStream`/`publishStream` and before subscribing to remote streams
* in the room.
*
* @param {string} streamRoomId ID of the Stream Room to join
*/
async joinStreamRoom(streamRoomId) {
return this.native.joinStreamRoom(this.servicePtr, [streamRoomId]);
}
/**
* Leaves a Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room to leave
*/
async leaveStreamRoom(streamRoomId) {
return this.native.leaveStreamRoom(this.servicePtr, [streamRoomId]);
}
/**
* Enables server-side recording for the Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room
*/
async enableStreamRoomRecording(streamRoomId) {
return this.native.enableStreamRoomRecording(this.servicePtr, [streamRoomId]);
}
/**
* Gets encryption keys used for Stream Room recordings.
*
* @param {string} streamRoomId ID of the Stream Room
* @returns {EndpointTypes.RecordingEncKey[]} list of recording encryption keys
*/
async getStreamRoomRecordingKeys(streamRoomId) {
return this.native.getStreamRoomRecordingKeys(this.servicePtr, [streamRoomId]);
}
/**
* Gets a single Stream Room by given Stream Room ID.
*
* @param {string} streamRoomId ID of the Stream Room to get
* @returns {StreamRoom} information about the Stream Room
*/
async getStreamRoom(streamRoomId) {
return this.native.getStreamRoom(this.servicePtr, [streamRoomId]);
}
/**
* Deletes a Stream Room by given Stream Room ID.
*
* @param {string} streamRoomId ID of the Stream Room to delete
*/
async deleteStreamRoom(streamRoomId) {
return this.native.deleteStreamRoom(this.servicePtr, [streamRoomId]);
}
/**
* Creates a local Stream handle for publishing media in given Stream Room.
*
* Call `addStreamTrack`/`removeStreamTrack` to stage tracks and `publishStream`/`updateStream` to send
* changes to the server.
*
* @param {string} streamRoomId ID of the Stream Room to create the stream in
* @returns {StreamHandle} handle to a local Stream instance
*/
async createStream(streamRoomId) {
const meta = {};
// tutaj uzupelniajac opcjonalne pola obiektu meta mozemy ustawiac w Janusie dodatkowe rzeczy
const handle = await this.native.createStream(this.servicePtr, [streamRoomId]);
this.streams.set(handle, { handle, streamRoomId, createStreamMeta: meta, remote: false });
return handle;
}
/**
* Gets a list of currently published streams in given Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room to list streams from
* @returns {StreamInfo[]} list of StreamInfo structs describing currently published streams
*/
async listStreams(streamRoomId) {
const remoteStreams = await this.native.listStreams(this.servicePtr, [streamRoomId]);
return remoteStreams;
}
/**
* Adds a local media track definition to a Stream handle.
*
* The track is staged locally and becomes visible to others after `publishStream`/`updateStream`.
*
* @param {StreamHandle} streamHandle handle returned by `createStream`
* @param {Types.StreamTrackInit} meta track/data channel metadata (track: `MediaStreamTrack`, dataChannel: `DataChannelMeta`)
* @returns {string} StreamTrackId assigned locally for this track
* @throws {Error} when the given `streamHandle` does not exist or the same browser track is already staged
*/
async addStreamTrack(streamHandle, meta) {
if (!this.streams.has(streamHandle)) {
throw new Error("[addStreamTrack]: there is no Stream with given Id: " + streamHandle);
}
let alreadyAddedId = "";
const tracksByHandle = Array.from(this.streamTracks.values()).filter((x) => x.streamHandle === streamHandle);
for (const streamTrack of tracksByHandle) {
if (streamTrack.track && streamTrack.track.id === meta.track?.id) {
if (streamTrack.markedToRemove === true) {
streamTrack.markedToRemove = undefined;
alreadyAddedId = streamTrack.id;
break;
}
else {
throw new Error("[addStreamTrack] StreamTrack with given browser's track already added.");
}
}
}
if (alreadyAddedId.length > 0) {
return alreadyAddedId;
}
const stream = this.streams.get(streamHandle);
if (!stream) {
throw new Error("Cannot find stream by id");
}
const streamTrackId = Utils_1.Utils.getRandomString(16);
const streamTrack = {
id: streamTrackId,
streamHandle: streamHandle,
track: meta.track,
dataChannelMeta: { created: meta.createDataChannel },
published: false,
};
this.streamTracks.set(streamTrackId, streamTrack);
return streamTrackId;
}
/**
* Removes a previously added media track from a Stream handle.
*
* For already published streams the removal is applied on `updateStream`.
*
* @param {StreamHandle} streamHandle handle returned by `createStream`
* @param {Types.StreamTrackInit} meta media track metadata previously passed to `addStreamTrack`
* @throws {Error} when the given `streamHandle` does not exist
*/
async removeStreamTrack(streamHandle, meta) {
if (!this.streams.has(streamHandle)) {
throw new Error("[removeStreamTrack]: there is no Stream with given Id: " + streamHandle);
}
for (const [key, streamTrack] of this.streamTracks.entries()) {
if (streamTrack.track &&
streamTrack.track?.id === meta.track?.id &&
streamTrack.streamHandle === streamHandle) {
streamTrack.markedToRemove = true;
}
}
}
/**
* Publishes the Stream (with currently staged tracks) to the server.
*
* @param {StreamHandle} streamHandle handle returned by `createStream`
* @param {(state: RTCPeerConnectionState) => void} onStreamState optional callback invoked on RTCPeerConnection state changes
* @returns {StreamPublishResult} result of the publish operation
* @throws {Error} when the given `streamHandle` does not exist
*/
async publishStream(streamHandle, onStreamState) {
const mediaTracks = [];
const dataTracks = [];
for (const value of this.streamTracks.values()) {
let toPublish = false;
if (value.streamHandle === streamHandle &&
value.track &&
!value.markedToRemove &&
value.published === false) {
mediaTracks.push(value.track);
// value.published = true;
toPublish = true;
}
if (value.streamHandle === streamHandle &&
value.dataChannelMeta.created === true &&
!value.markedToRemove &&
value.published === false) {
dataTracks.push(value);
toPublish = true;
}
value.published = toPublish;
}
const _stream = this.streams.get(streamHandle);
if (!_stream) {
throw new Error("No stream defined to publish");
}
_stream.localMediaStream =
mediaTracks.length > 0 ? new MediaStream(mediaTracks) : undefined;
const turnCredentials = await this.native.getTurnCredentials(this.servicePtr, []);
await this.client.setTurnCredentials(turnCredentials);
await this.client.createPeerConnectionWithLocalStream(streamHandle, _stream.streamRoomId, _stream.localMediaStream, dataTracks);
if (onStreamState && typeof onStreamState === "function") {
this.client
.getStreamStateChangeDispatcher()
.addOnStateChangeListener({ streamHandle: streamHandle }, (event) => onStreamState(event.state));
}
const res = await this.native.publishStream(this.servicePtr, [streamHandle]);
return res;
}
/**
* Updates a published Stream after adding/removing tracks.
*
* @param {StreamHandle} streamHandle handle returned by `createStream`
* @returns {StreamPublishResult} result of the update operation
* @throws {Error} when the given `streamHandle` does not exist
*/
async updateStream(streamHandle) {
// configure client
const tracksToAdd = [];
const tracksToRemove = [];
for (const value of this.streamTracks.values()) {
if (value.streamHandle === streamHandle && value.track) {
if (!value.published && !value.markedToRemove) {
tracksToAdd.push(value.track);
}
if (value.markedToRemove) {
tracksToRemove.push(value.track);
}
}
}
const _stream = this.streams.get(streamHandle);
if (!_stream) {
throw new Error("No stream defined to publish");
}
const turnCredentials = await this.native.getTurnCredentials(this.servicePtr, []);
await this.client.setTurnCredentials(turnCredentials);
await this.client.updatePeerConnectionWithLocalStream(_stream.streamRoomId, _stream.localMediaStream, tracksToAdd, tracksToRemove);
const res = await this.native.updateStream(this.servicePtr, [streamHandle]);
return res;
}
filterMapByValue(map, predicate) {
const result = new Map();
for (const [key, value] of map) {
if (predicate(value, key)) {
result.set(key, value);
}
}
return result;
}
/**
* Stops publishing the Stream.
*
* @param {StreamHandle} streamHandle handle returned by `createStream`
* @throws {Error} when the given `streamHandle` does not exist
*/
async unpublishStream(streamHandle) {
if (!this.streams.has(streamHandle)) {
throw new Error("No local stream with given id to unpublish");
}
const _stream = this.streams.get(streamHandle);
const filteredTracks = this.filterMapByValue(this.streamTracks, (x) => x.streamHandle !== streamHandle);
this.streamTracks = filteredTracks;
await this.native.unpublishStream(this.servicePtr, [streamHandle]);
this.client.removeSenderPeerConnectionOnUnpublish(_stream.streamRoomId, _stream.localMediaStream);
this.streams.delete(streamHandle);
this.client.getStreamStateChangeDispatcher().removeOnStateChangeListener({ streamHandle });
}
/**
* Subscribes to selected remote streams (and optionally specific tracks) in the Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room
* @param {EndpointTypes.StreamSubscription[]} subscriptions list of remote streams/tracks to subscribe to
*/
async subscribeToRemoteStreams(streamRoomId, subscriptions) {
// native part
const peerCredentials = await this.native.getTurnCredentials(this.servicePtr, []);
await this.client.setTurnCredentials(peerCredentials);
// server / core part
await this.native.subscribeToRemoteStreams(this.servicePtr, [streamRoomId, subscriptions]);
this.client.getConnectionManager().initialize(streamRoomId, "subscriber");
}
/**
* Modifies current remote streams subscriptions.
*
* @param {string} streamRoomId ID of the Stream Room
* @param {EndpointTypes.StreamSubscription[]} subscriptionsToAdd list of subscriptions to add
* @param {StreamSubscription[]} subscriptionsToRemove list of subscriptions to remove
*/
async modifyRemoteStreamsSubscriptions(streamRoomId, subscriptionsToAdd, subscriptionsToRemove) {
await this.native.modifyRemoteStreamsSubscriptions(this.servicePtr, [
streamRoomId,
subscriptionsToAdd,
subscriptionsToRemove,
]);
}
/**
* Unsubscribes from selected remote streams (and optionally specific tracks) in the Stream Room.
*
* @param {string} streamRoomId ID of the Stream Room
* @param {StreamSubscription[]} subscriptions list of subscriptions to remove
*/
async unsubscribeFromRemoteStreams(streamRoomId, subscriptions) {
await this.native.unsubscribeFromRemoteStreams(this.servicePtr, [
streamRoomId,
subscriptions,
]);
}
/**
* Registers a listener for remote tracks in the Stream Room.
*
* @param {RemoteStreamListener} listener listener configuration
* @param {string} listener.streamRoomId ID of the Stream Room
* @param {number} [listener.streamId] optional remote Stream ID to filter events (omit for all streams)
* @param {(event: RTCTrackEvent) => void} listener.onRemoteStreamTrack callback invoked for incoming remote tracks
*/
addRemoteStreamListener(listener) {
this.client.addRemoteStreamListener(listener);
}
/**
* Subscribe for the Stream Room events on the given subscription query.
*
* @param {string[]} subscriptionQueries list of queries
* @return list of subscriptionIds in maching order to subscriptionQueries
*/
async subscribeFor(subscriptionQueries) {
return this.native.subscribeFor(this.servicePtr, [subscriptionQueries]);
}
/**
* Unsubscribe from events for the given subscriptionId.
* @param {string[]} subscriptionIds list of subscriptionId
*/
async unsubscribeFrom(subscriptionIds) {
return this.native.unsubscribeFrom(this.servicePtr, [subscriptionIds]);
}
/**
* Generate subscription Query for the Stream Room events.
* @param {EventType} eventType type of event which you listen for
* @param {EventSelectorType} selectorType scope on which you listen for events
* @param {string} selectorId ID of the selector
* @returns {string} subscription ID
*/
async buildSubscriptionQuery(eventType, selectorType, selectorId) {
return this.native.buildSubscriptionQuery(this.servicePtr, [
eventType,
selectorType,
selectorId,
]);
}
/**
* Registers a callback for audio level statistics produced by the WebRTC client.
*
* @param {(stats: AudioLevelsStats) => void} onStats callback invoked with current audio levels stats
*/
async addAudioLevelStatsListener(onStats) {
if (onStats && typeof onStats === "function") {
this.client.setAudioLevelCallback(onStats);
}
}
/**
* Sends binary data over a WebRTC DataChannel associated with a published Stream data track.
*
* @param {Types.StreamTrackId} streamTrackId StreamTrackId of the data track created via `addStreamTrack`
* @param {Uint8Array} data bytes to send to remote participants
* @throws {Error} when there is no DataTrack (or DataChannel) for the given `streamTrackId`
*/
async sendData(streamTrackId, data) {
const dataChannel = this.streamTracks.get(streamTrackId)?.dataChannelMeta.dataChannel;
if (!dataChannel) {
throw new Error(`There is no DataTrack with given streamTrackId: ${streamTrackId}`);
}
const frame = await this.client.encryptDataChannelData(data);
dataChannel.send(frame);
}
}
exports.StreamApi = StreamApi;