opentok
Version:
OpenTok server-side SDK
469 lines (413 loc) • 14.6 kB
JavaScript
var errors = require('./errors');
var { api } = require('./api')
/**
* An object representing an OpenTok archive.
* <p>
* Do not call the <code>new()</code> constructor. To start recording an archive, call the
* {@link OpenTok#startArchive OpenTok.startArchive()} method.
*
* @property {Number} createdAt
* The time at which the archive was created, in milliseconds since the UNIX epoch.
*
* @property {String} duration
* The duration of the archive, in seconds.
*
* @property {Boolean} hasAudio
* Whether the archive has an audio track (<code>true</code>) or not (<code>false</code>).
* You can prevent audio from being recorded by setting
* <code>hasAudio</code> to <code>false</code>
* in the <code>options</code> parameter you pass into the
* {@link OpenTok#startArchive OpenTok.startArchive()} method.
*
* @property {Boolean} hasVideo
* Whether the archive has an video track (<code>true</code>) or not (<code>false</code>).
* You can prevent video from being recorded by setting
* <code>hasVideo</code> to <code>false</code>
* in the <code>options</code> parameter you pass into the
* {@link OpenTok#startArchive OpenTok.startArchive()} method.
*
* @property {String} id
* The archive ID.
*
* @property {String} name
* The name of the archive. If no name was provided when the archive was created, this is set
* to null.
*
* @property {String} streamMode
* The stream mode for the archive. This can be set to one of the the following:
*
* <ul>
* <li> "auto" — streams included in the archive are selected automatically
* (the default).</li>
*
* <li> "manual" — Specify streams to be included based on calls to the
* {@link OpenTok#addArchiveStream OpenTok.addArchiveStream()} and
* {@link OpenTok#removeArchiveStream OpenTok.removeArchiveStream()} methods.</li>
* </ul>
*
* @property {String} outputMode
* The output mode to be generated for this archive, which can be one of the following:
* <ul>
* <li> "composed" -- All streams in the archive are recorded to a single (composed) file.
* <li> "individual" -- Each stream in the archive is recorded to its own individual file.
* </ul>
*
* See the {@link OpenTok#startArchive OpenTok.startArchive()} method.
*
* @property {String} projectId
* The API key associated with the archive.
*
* @property {String} reason
* For archives with the status "stopped" or "failed", this string describes the reason
* the archive stopped (such as "maximum duration exceeded") or failed.
*
* @property {String} resolution The resolution of the archive (either "640x480", "1280x720"
* or "1920x1080").
* This property is only set for composed archives.
*
* @property {String} sessionId
* The session ID of the OpenTok session associated with this archive.
*
* @property {Number} size
* The size of the MP4 file. For archives that have not been generated, this value is set to 0.
*
* @property {String} status
* The status of the archive, which can be one of the following:
* <ul>
* <li> "available" -- The archive is available for download from the OpenTok cloud.
* <li> "expired" -- The archive is no longer available for download from the OpenTok cloud.
* <li> "failed" -- The archive recording failed.
* <li> "paused" -- The archive is in progress and no clients are publishing streams to
* the session. When an archive is in progress and any client publishes a stream,
* the status is "started". When an archive is "paused", nothing is recorded. When
* a client starts publishing a stream, the recording starts (or resumes). If all clients
* disconnect from a session that is being archived, the status changes to "paused", and
* after 60 seconds the archive recording stops (and the status changes to "stopped").</li>
* <li> "started" -- The archive started and is in the process of being recorded.
* <li> "stopped" -- The archive stopped recording.
* <li> "uploaded" -- The archive is available for download from the the upload target
* Amazon S3 bucket or Windows Azure container you set up for your
* <a href="https://tokbox.com/account">OpenTok project</a>.
* </ul>
*
* @property {String} url
* The download URL of the available MP4 file. This is only set for an archive with the status set
* to "available"; for other archives, (including archives with the status "uploaded") this
* property is set to null. The download URL is obfuscated, and the file is only available from
* the URL for 10 minutes. To generate a new URL, call the
* {@link OpenTok#getArchive OpenTok.getArchive()} or
* {@link OpenTok#listArchives OpenTok.listArchives()} method.
*
* @property {String} multiArchiveTag
* Set this to support recording multiple archives for the same session simultaneously. Set
* this to a unique string for each simultaneous archive of an ongoing session. You must also
* set this option when manually starting an archive that is automatically archived. Note that
* the multiArchiveTag value is not included in the response for the methods to list archives
* and retrieve archive information. If you do not specify a unique multiArchiveTag, you can
* only record one archive at a time for a given session.
* See <a href="https://tokbox.com/developer/guides/archiving/#simultaneous-archives">
* Simultaneous archives</a>.
*
* @see {@link OpenTok#deleteArchive OpenTok.deleteArchive()}
* @see {@link OpenTok#getArchive OpenTok.getArchive()}
* @see {@link OpenTok#startArchive OpenTok.startArchive()}
* @see {@link OpenTok#stopArchive OpenTok.stopArchive()}
* @see {@link OpenTok#listArchives OpenTok.listArchives()}
*
* @class Archive
*/
function Archive(config, properties) {
var hasProp = {}.hasOwnProperty;
var id = properties.id;
var key;
for (key in properties) {
if (hasProp.call(properties, key)) {
this[key] = properties[key];
}
}
/**
* Stops the recording of the archive.
* <p>
* Archives automatically stop recording after 120 minutes or when all clients have disconnected
* from the session being archived.
*
* @param callback {Function} The function to call upon completing the operation. Two arguments
* are passed to the function:
*
* <ul>
*
* <li>
* <code>error</code> — An error object (if the call to the method fails).
* </li>
*
* <li>
* <code>archive</code> — The Archive object.
* </li>
*
* </ul>
*
* @method #stop
* @memberof Archive
*/
this.stop = function (callback) {
exports.stopArchive(config, id, callback);
};
/**
* Deletes the OpenTok archive.
* <p>
* You can only delete an archive which has a status of "available" or "uploaded". Deleting an
* archive removes its record from the list of archives. For an "available" archive, it also
* removes the archive file, making it unavailable for download.
*
* @param callback {Function} The function to call upon completing the operation. On successfully
* deleting the archive, the function is called with no arguments passed in. On failure, an error
* object is passed into the function.
*
* @method #delete
* @memberof Archive
*/
this.delete = function (callback) {
exports.deleteArchive(config, id, callback);
};
}
exports.listArchives = function (config, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to listArchives');
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive`);
if (options.offset) {
archiveUrl.searchParams.set(
'offset',
options.offset,
)
}
if (options.count) {
archiveUrl.searchParams.set(
'count',
options.count,
)
}
if (options.sessionId) {
archiveUrl.searchParams.set(
'sessionId',
options.sessionId,
)
}
const parseResponse = (err, body, response) => {
if (err) {
callback(err);
return;
}
callback(
null,
body?.items.map((item) => new Archive(config, item)) || [],
body.count || 0
);
}
api({
config: config,
method: 'GET',
url: archiveUrl.toString(),
callback: parseResponse,
});
};
exports.startArchive = function (ot, config, sessionId, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to startArchive');
}
if (!sessionId) {
callback(new errors.ArgumentError('No session ID given'));
return;
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive`);
//const oldApi = function (config, method, path, body, callback) {
const startArchiveCallback = (err, body, response) => {
if (err && response.status === 404) {
callback(new errors.ArchiveError('Session not found'));
return;
}
if (err) {
callback(
err,
response.status === 409
? new errors.ArchiveError('Recording already in progress or session not using OpenTok Media Router')
: err,
);
return;
}
if (body.status !== 'started') {
callback(new errors.RequestError('Unexpected response from OpenTok: ' + JSON.stringify(body || { status: response.status, statusMessage: response.statusMessage })));
return;
}
callback(null, new Archive(config, body));
}
const requestBody = {
sessionId: sessionId,
...options,
};
api({
config: config,
method: 'POST',
url: archiveUrl.toString(),
body: requestBody,
callback: startArchiveCallback
})
};
exports.stopArchive = function (config, archiveId, callback) {
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to stopArchive');
}
if (!archiveId) {
callback(new errors.ArgumentError('No archive ID given'));
return;
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}/stop`);
const stopArchiveCallback = (err, body, response) => {
if (!err) {
callback(null, new Archive(config, body));
return;
}
switch (response?.status) {
case 404:
callback(new errors.ArchiveError('Archive not found'));
break;
case 409:
callback(new errors.ArchiveError(body?.message || 'Unknown archive error'));
break;
default:
callback(err);
}
}
api({
config: config,
method: 'POST',
url: archiveUrl.toString(),
body: {},
callback: stopArchiveCallback,
})
};
exports.getArchive = function (config, archiveId, callback) {
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to getArchive');
}
if (!archiveId) {
callback(new errors.ArgumentError('No archive ID given'));
return;
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}`);
const getArchiveCallback = (err, body, response) => {
if (err && response.status === 404) {
err.message = 'Archive not found';
}
if (err) {
callback(err);
return;
}
callback(null, new Archive(config, body));
}
api({
config: config,
method: 'GET',
url: archiveUrl.toString(),
callback: getArchiveCallback,
});
};
function patchArchive(config, archiveId, options, callback) {
if (!archiveId) {
callback(new errors.ArgumentError('No Archive ID given'));
return;
}
if (options.addStream && options.removeStream) {
callback(new errors.ArgumentError('You cannot have both addStream and removeStream'));
}
if (!options.addStream && !options.removeStream) {
callback(new errors.ArgumentError('Need one of addStream or removeStream'));
}
// Coerce to boolean
if (options.addStream) {
options.hasAudio = Boolean(options.hasAudio);
options.hasVideo = Boolean(options.hasVideo);
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}/streams`);
const patchArchiveCallback = (err, body, response) => {
switch (response.status) {
case 400:
callback(new errors.ArgumentError('Invalid request: ' + JSON.stringify(body)));
return;
case 405:
callback(new errors.ArchiveError('Unsupported Stream Mode'));
return;
case 404:
callback(new errors.ArchiveError('Archive or stream not found'));
return;
}
if (err) {
callback(err);
return;
}
callback(null)
}
api({
config: config,
method: 'PATCH',
url: archiveUrl.toString(),
callback: patchArchiveCallback,
body: options,
});
}
exports.addArchiveStream = function (config, archiveId, streamId, archiveOptions, callback) {
if (typeof archiveOptions === 'function') {
callback = archiveOptions;
archiveOptions = {};
}
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to addArchiveStream');
}
patchArchive(
config,
archiveId,
{
hasAudio: archiveOptions?.hasOwnProperty('hasAudio') ? archiveOptions.hasAudio : true,
hasVideo: archiveOptions?.hasOwnProperty('hasVideo') ? archiveOptions.hasVideo : true,
addStream: streamId
},
callback,
);
};
exports.removeArchiveStream = function (config, archiveId, streamId, callback) {
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to removeArchiveStream');
}
patchArchive(config, archiveId, { removeStream: streamId }, callback);
};
exports.deleteArchive = function (config, archiveId, callback) {
if (typeof callback !== 'function') {
throw new errors.ArgumentError('No callback given to deleteArchive');
}
if (!archiveId) {
callback(new errors.ArgumentError('No archive ID given'));
return;
}
const archiveUrl = new URL(`${config.apiEndpoint}/v2/project/${config.apiKey}/archive/${encodeURIComponent(archiveId)}`);
const deleteArchiveCallback = (err, body, response) => {
if (response.status === 404) {
callback(new errors.ArchiveError('Archive not found'));
return;
}
callback(err);
}
api({
config: config,
method: 'DELETE',
url: archiveUrl.toString(),
callback: deleteArchiveCallback,
});
};