@appium/support
Version:
Support libs used across appium packages
185 lines • 6.76 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MJpegStream = void 0;
const lodash_1 = __importDefault(require("lodash"));
const logger_1 = __importDefault(require("./logger"));
const bluebird_1 = __importDefault(require("bluebird"));
const image_util_1 = require("./image-util");
const stream_1 = require("stream");
const node_1 = require("./node");
const axios_1 = __importDefault(require("axios"));
// lazy load this, as it might not be available
let MJpegConsumer = null;
/**
* @throws {Error} If `mjpeg-consumer` module is not installed or cannot be loaded
*/
async function initMJpegConsumer() {
if (!MJpegConsumer) {
try {
MJpegConsumer = await (0, node_1.requirePackage)('mjpeg-consumer');
}
catch { }
}
if (!MJpegConsumer) {
throw new Error('mjpeg-consumer module is required to use MJPEG-over-HTTP features. ' +
'Please install it first (npm i -g mjpeg-consumer) and restart Appium.');
}
}
// amount of time to wait for the first image in the stream
const MJPEG_SERVER_TIMEOUT_MS = 10000;
/** Class which stores the last bit of data streamed into it */
class MJpegStream extends stream_1.Writable {
/**
* Create an MJpegStream
* @param {string} mJpegUrl - URL of MJPEG-over-HTTP stream
* @param {function} [errorHandler=noop] - additional function that will be
* called in the case of any errors.
* @param {object} [options={}] - Options to pass to the Writable constructor
*/
constructor(mJpegUrl, errorHandler = lodash_1.default.noop, options = {}) {
super(options);
/**
* @type {number}
*/
this.updateCount = 0;
this.errorHandler = errorHandler;
this.url = mJpegUrl;
this.clear();
}
/**
* Get the base64-encoded version of the JPEG
*
* @returns {?string} base64-encoded JPEG image data
* or `null` if no image can be parsed
*/
get lastChunkBase64() {
const lastChunk = /** @type {Buffer} */ (this.lastChunk);
return !lodash_1.default.isEmpty(this.lastChunk) && lodash_1.default.isBuffer(this.lastChunk)
? lastChunk.toString('base64')
: null;
}
/**
* Get the PNG version of the JPEG buffer
*
* @returns {Promise<Buffer?>} PNG image data or `null` if no PNG
* image can be parsed
*/
async lastChunkPNG() {
const lastChunk = /** @type {Buffer} */ (this.lastChunk);
if (lodash_1.default.isEmpty(lastChunk) || !lodash_1.default.isBuffer(lastChunk)) {
return null;
}
try {
return await (0, image_util_1.requireSharp)()(lastChunk).png().toBuffer();
}
catch {
return null;
}
}
/**
* Get the base64-encoded version of the PNG
*
* @returns {Promise<string?>} base64-encoded PNG image data
* or `null` if no image can be parsed
*/
async lastChunkPNGBase64() {
const png = await this.lastChunkPNG();
return png ? png.toString('base64') : null;
}
/**
* Reset internal state
*/
clear() {
this.registerStartSuccess = null;
this.registerStartFailure = null;
this.responseStream = null;
this.consumer = null;
this.lastChunk = null;
this.updateCount = 0;
}
/**
* Start reading the MJpeg stream and storing the last image
*/
async start(serverTimeout = MJPEG_SERVER_TIMEOUT_MS) {
// ensure we're not started already
this.stop();
await initMJpegConsumer();
this.consumer = new MJpegConsumer();
const url = this.url;
try {
this.responseStream = (await (0, axios_1.default)({
url,
responseType: 'stream',
timeout: serverTimeout,
})).data;
}
catch (e) {
throw new Error(`Cannot connect to the MJPEG stream at ${url}. ` +
`Original error: ${lodash_1.default.has(e, 'response') ? JSON.stringify(e.response) : /** @type {Error} */ (e).message}`);
}
const onErr = (/** @type {Error} */ err) => {
// Make sure we don't get an outdated screenshot if there was an error
this.lastChunk = null;
logger_1.default.error(`Error getting MJpeg screenshot chunk: ${err.message}`);
this.errorHandler(err);
if (this.registerStartFailure) {
this.registerStartFailure(err);
}
};
const onClose = () => {
logger_1.default.debug(`The connection to MJPEG server at ${url} has been closed`);
this.lastChunk = null;
};
// use the deferred pattern so we can wait for the start of the stream
// based on what comes in from an external pipe
const startPromise = new bluebird_1.default((res, rej) => {
this.registerStartSuccess = res;
this.registerStartFailure = rej;
})
// start a timeout so that if the server does not return data, we don't
// block forever.
.timeout(serverTimeout, `Waited ${serverTimeout}ms but the MJPEG server never sent any images`);
this.responseStream
.once('close', onClose)
.on('error', onErr) // ensure we do something with errors
.pipe(this.consumer) // allow chunking and transforming of jpeg data
.pipe(this); // send the actual jpegs to ourself
await startPromise;
}
/**
* Stop reading the MJpeg stream. Ensure we disconnect all the pipes and stop
* the HTTP request itself. Then reset the state.
*/
stop() {
if (this.consumer) {
this.consumer.unpipe(this);
}
if (this.responseStream) {
if (this.consumer) {
this.responseStream.unpipe(this.consumer);
}
this.responseStream.destroy();
}
this.clear();
}
/**
* Override the Writable write() method in order to save the last image and
* log the number of images we have received
* @override
* @param {Buffer} data - binary data streamed from the MJpeg consumer
*/
write(data) {
this.lastChunk = data;
this.updateCount++;
if (this.registerStartSuccess) {
this.registerStartSuccess();
this.registerStartSuccess = null;
}
return true;
}
}
exports.MJpegStream = MJpegStream;
//# sourceMappingURL=mjpeg.js.map