How to use initGstreamerPipeline method in Appium Android Driver

Best JavaScript code snippet using appium-android-driver

Run Appium Android Driver automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

streamscreen.js

Source: streamscreen.js Github

copy
1import _ from 'lodash';
2import { fs, system, logger, util } from 'appium-support';
3import log from '../logger';
4import { exec, SubProcess } from 'teen_process';
5import { checkPortStatus } from 'portscanner';
6import http from 'http';
7import net from 'net';
8import B from 'bluebird';
9import { waitForCondition } from 'asyncbox';
10import { spawn } from 'child_process';
11import url from 'url';
12
13const commands = {};
14
15const RECORDING_INTERVAL_SEC = 5;
16const STREAMING_STARTUP_TIMEOUT_MS = 5000;
17const GSTREAMER_BINARY = `gst-launch-1.0${system.isWindows() ? '.exe' : ''}`;
18const GST_INSPECT_BINARY = `gst-inspect-1.0${system.isWindows() ? '.exe' : ''}`;
19const REQUIRED_GST_PLUGINS = {
20  avdec_h264: 'gst-libav',
21  h264parse: 'gst-plugins-bad',
22  jpegenc: 'gst-plugins-good',
23  tcpserversink: 'gst-plugins-base',
24  multipartmux: 'gst-plugins-good',
25};
26const SCREENRECORD_BINARY = 'screenrecord';
27const GST_TUTORIAL_URL = 'https://gstreamer.freedesktop.org/documentation/installing/index.html';
28const DEFAULT_HOST = '127.0.0.1';
29const TCP_HOST = '127.0.0.1';
30const DEFAULT_PORT = 8093;
31const DEFAULT_QUALITY = 70;
32const DEFAULT_BITRATE = 4000000; // 4 Mbps
33const BOUNDARY_STRING = '--2ae9746887f170b8cf7c271047ce314c';
34
35const ADB_SCREEN_STREAMING_FEATURE = 'adb_screen_streaming';
36
37function createStreamingLogger (streamName, udid) {
38  return logger.getLogger(`${streamName}@` + _.truncate(udid, {
39    length: 8,
40    omission: '',
41  }));
42}
43
44async function verifyStreamingRequirements (adb) {
45  if (!_.trim(await adb.shell(['which', SCREENRECORD_BINARY]))) {
46    throw new Error(
47      `The required '${SCREENRECORD_BINARY}' binary is not available on the device under test`);
48  }
49
50  const gstreamerCheckPromises = [];
51  for (const binaryName of [GSTREAMER_BINARY, GST_INSPECT_BINARY]) {
52    gstreamerCheckPromises.push((async () => {
53      try {
54        await fs.which(binaryName);
55      } catch (e) {
56        throw new Error(`The '${binaryName}' binary is not available in the PATH on the host system. ` +
57          `See ${GST_TUTORIAL_URL} for more details on how to install it.`);
58      }
59    })());
60  }
61  await B.all(gstreamerCheckPromises);
62
63  const moduleCheckPromises = [];
64  for (const [name, modName] of _.toPairs(REQUIRED_GST_PLUGINS)) {
65    moduleCheckPromises.push((async () => {
66      const {stdout} = await exec(GST_INSPECT_BINARY, [name]);
67      if (!_.includes(stdout, modName)) {
68        throw new Error(
69          `The required GStreamer plugin '${name}' from '${modName}' module is not installed. ` +
70          `See ${GST_TUTORIAL_URL} for more details on how to install it.`);
71      }
72    })());
73  }
74  await B.all(moduleCheckPromises);
75}
76
77async function getDeviceInfo (adb) {
78  const output = await adb.shell(['dumpsys', 'display']);
79  const result = {};
80  for (const [key, pattern] of [
81    ['width', /\bdeviceWidth=(\d+)/],
82    ['height', /\bdeviceHeight=(\d+)/],
83    ['fps', /\bfps=(\d+)/],
84  ]) {
85    const match = pattern.exec(output);
86    if (!match) {
87      log.debug(output);
88      throw new Error(`Cannot parse the device ${key} from the adb command output. ` +
89        `Check the server log for more details.`);
90    }
91    result[key] = parseInt(match[1], 10);
92  }
93  result.udid = adb.curDeviceId;
94  return result;
95}
96
97async function initDeviceStreamingProc (adb, deviceInfo, opts = {}) {
98  const {
99    width,
100    height,
101    bitRate,
102  } = opts;
103  const adjustedWidth = parseInt(width, 10) || deviceInfo.width;
104  const adjustedHeight = parseInt(height, 10) || deviceInfo.height;
105  const adjustedBitrate = parseInt(bitRate, 10) || DEFAULT_BITRATE;
106  let screenRecordCmd = SCREENRECORD_BINARY +
107    ` --output-format=h264` +
108    // 5 seconds is fine to detect rotation changes
109    ` --time-limit=${RECORDING_INTERVAL_SEC}`;
110  if (width || height) {
111    screenRecordCmd += ` --size=${adjustedWidth}x${adjustedHeight}`;
112  }
113  if (bitRate) {
114    screenRecordCmd += ` --bit-rate=${adjustedBitrate}`;
115  }
116  const adbArgs = [
117    ...adb.executable.defaultArgs,
118    'exec-out',
119    // The loop is required, because by default the maximum record duration
120    // for screenrecord is always limited
121    `while true; do ${screenRecordCmd} -; done`,
122  ];
123  const deviceStreaming = spawn(adb.executable.path, adbArgs);
124  deviceStreaming.on('exit', (code, signal) => {
125    log.debug(`Device streaming process exited with code ${code}, signal ${signal}`);
126  });
127
128  let isStarted = false;
129  const deviceStreamingLogger = createStreamingLogger(SCREENRECORD_BINARY, deviceInfo.udid);
130  const errorsListener = (chunk) => {
131    const stderr = chunk.toString();
132    if (_.trim(stderr)) {
133      deviceStreamingLogger.debug(stderr);
134    }
135  };
136  deviceStreaming.stderr.on('data', errorsListener);
137
138  const startupListener = (chunk) => {
139    if (!isStarted) {
140      isStarted = !_.isEmpty(chunk);
141    }
142  };
143  deviceStreaming.stdout.on('data', startupListener);
144
145  try {
146    log.info(`Starting device streaming: ${util.quote([adb.executable.path, ...adbArgs])}`);
147    await waitForCondition(() => isStarted, {
148      waitMs: STREAMING_STARTUP_TIMEOUT_MS,
149      intervalMs: 300,
150    });
151  } catch (e) {
152    log.errorAndThrow(
153      `Cannot start the screen streaming process. Original error: ${e.message}`);
154  } finally {
155    deviceStreaming.stderr.removeListener('data', errorsListener);
156    deviceStreaming.stdout.removeListener('data', startupListener);
157  }
158  return deviceStreaming;
159}
160
161async function initGstreamerPipeline (deviceStreamingProc, deviceInfo, opts = {}) {
162  const {
163    width,
164    height,
165    quality,
166    tcpPort,
167    considerRotation,
168    logPipelineDetails,
169  } = opts;
170  const adjustedWidth = parseInt(width, 10) || deviceInfo.width;
171  const adjustedHeight = parseInt(height, 10) || deviceInfo.height;
172  const gstreamerPipeline = new SubProcess(GSTREAMER_BINARY, [
173    '-v',
174    'fdsrc', 'fd=0',
175    '!', 'video/x-h264,' +
176      `width=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedWidth},` +
177      `height=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedHeight},` +
178      `framerate=${deviceInfo.fps}/1,` +
179      'byte-stream=true',
180    '!', 'h264parse',
181    '!', 'queue', 'leaky=downstream',
182    '!', 'avdec_h264',
183    '!', 'queue', 'leaky=downstream',
184    '!', 'jpegenc', `quality=${quality}`,
185    '!', 'multipartmux', `boundary=${BOUNDARY_STRING}`,
186    '!', 'tcpserversink', `host=${TCP_HOST}`, `port=${tcpPort}`,
187  ], {
188    stdio: [deviceStreamingProc.stdout, 'pipe', 'pipe']
189  });
190  gstreamerPipeline.on('exit', (code, signal) => {
191    log.debug(`Pipeline streaming process exited with code ${code}, signal ${signal}`);
192  });
193  const gstreamerLogger = createStreamingLogger('gst', deviceInfo.udid);
194  const gstOutputListener = (stdout, stderr) => {
195    if (_.trim(stderr || stdout)) {
196      gstreamerLogger.debug(stderr || stdout);
197    }
198  };
199  gstreamerPipeline.on('output', gstOutputListener);
200  let didFail = false;
201  try {
202    log.info(`Starting GStreamer pipeline: ${gstreamerPipeline.rep}`);
203    await gstreamerPipeline.start(0);
204    await waitForCondition(async () => {
205      try {
206        return (await checkPortStatus(tcpPort, TCP_HOST)) === 'open';
207      } catch (ign) {
208        return false;
209      }
210    }, {
211      waitMs: STREAMING_STARTUP_TIMEOUT_MS,
212      intervalMs: 300,
213    });
214  } catch (e) {
215    didFail = true;
216    log.errorAndThrow(
217      `Cannot start the screen streaming pipeline. Original error: ${e.message}`);
218  } finally {
219    if (!logPipelineDetails || didFail) {
220      gstreamerPipeline.removeListener('output', gstOutputListener);
221    }
222  }
223  return gstreamerPipeline;
224}
225
226function extractRemoteAddress (req) {
227  return req.headers['x-forwarded-for']
228    || req.socket.remoteAddress
229    || req.connection.remoteAddress
230    || req.connection.socket.remoteAddress;
231}
232
233
234/**
235 * @typedef {Object} StartScreenStreamingOptions
236 *
237 * @property {?number} width - The scaled width of the device's screen. If unset then the script will assign it
238 * to the actual screen width measured in pixels.
239 * @property {?number} height - The scaled height of the device's screen. If unset then the script will assign it
240 * to the actual screen height measured in pixels.
241 * @property {?number} bitRate - The video bit rate for the video, in bits per second.
242 * The default value is 4000000 (4 Mb/s). You can increase the bit rate to improve video quality,
243 * but doing so results in larger movie files.
244 * @property {?string} host [127.0.0.1] - The IP address/host name to start the MJPEG server on.
245 * You can set it to `0.0.0.0` to trigger the broadcast on all available network interfaces.
246 * @property {?string} pathname - The HTTP request path the MJPEG server should be available on.
247 * If unset then any pathname on the given `host`/`port` combination will work. Note that the value
248 * should always start with a single slash: `/`
249 * @property {?number} tcpPort [8094] - The port number to start the internal TCP MJPEG broadcast on.
250 * This type of broadcast always starts on the loopback interface (`127.0.0.1`).
251 * @property {?number} port [8093] - The port number to start the MJPEG server on.
252 * @property {?number} quality [70] - The quality value for the streamed JPEG images.
253 * This number should be in range [1, 100], where 100 is the best quality.
254 * @property {?boolean} considerRotation [false] - If set to `true` then GStreamer pipeline will
255 * increase the dimensions of the resulting images to properly fit images in both landscape and
256 * portrait orientations. Set it to `true` if the device rotation is not going to be the same during the
257 * broadcasting session.
258 * @property {?boolean} logPipelineDetails [false] - Whether to log GStreamer pipeline events into
259 * the standard log output. Might be useful for debugging purposes.
260 */
261
262/**
263 * Starts device screen broadcast by creating MJPEG server.
264 * Multiple calls to this method have no effect unless the previous streaming
265 * session is stopped.
266 * This method only works if the `adb_screen_streaming` feature is
267 * enabled on the server side.
268 *
269 * @param {?StartScreenStreamingOptions} options - The available options.
270 * @throws {Error} If screen streaming has failed to start or
271 * is not supported on the host system or
272 * the corresponding server feature is not enabled.
273 */
274commands.mobileStartScreenStreaming = async function mobileStartScreenStreaming (options = {}) {
275  this.ensureFeatureEnabled(ADB_SCREEN_STREAMING_FEATURE);
276
277  const {
278    width,
279    height,
280    bitRate,
281    host = DEFAULT_HOST,
282    port = DEFAULT_PORT,
283    pathname,
284    tcpPort = DEFAULT_PORT + 1,
285    quality = DEFAULT_QUALITY,
286    considerRotation = false,
287    logPipelineDetails = false,
288  } = options;
289
290  if (_.isUndefined(this._screenStreamingProps)) {
291    await verifyStreamingRequirements(this.adb);
292  }
293  if (!_.isEmpty(this._screenStreamingProps)) {
294    log.info(`The screen streaming session is already running. ` +
295      `Stop it first in order to start a new one.`);
296    return;
297  }
298  if ((await checkPortStatus(port, host)) === 'open') {
299    log.info(`The port #${port} at ${host} is busy. ` +
300      `Assuming the screen streaming is already running`);
301    return;
302  }
303  if ((await checkPortStatus(tcpPort, TCP_HOST)) === 'open') {
304    log.errorAndThrow(`The port #${tcpPort} at ${TCP_HOST} is busy. ` +
305      `Make sure there are no leftovers from previous sessions.`);
306  }
307  this._screenStreamingProps = null;
308
309  const deviceInfo = await getDeviceInfo(this.adb);
310  const deviceStreamingProc = await initDeviceStreamingProc(this.adb, deviceInfo, {
311    width,
312    height,
313    bitRate,
314  });
315  let gstreamerPipeline;
316  try {
317    gstreamerPipeline = await initGstreamerPipeline(deviceStreamingProc, deviceInfo, {
318      width,
319      height,
320      quality,
321      tcpPort,
322      considerRotation,
323      logPipelineDetails,
324    });
325  } catch (e) {
326    if (deviceStreamingProc.kill(0)) {
327      deviceStreamingProc.kill();
328    }
329    throw e;
330  }
331
332  let mjpegSocket;
333  let mjpegServer;
334  try {
335    await new B((resolve, reject) => {
336      mjpegSocket = net.createConnection(tcpPort, TCP_HOST, () => {
337        log.info(`Successfully connected to MJPEG stream at tcp://${TCP_HOST}:${tcpPort}`);
338        mjpegServer = http.createServer((req, res) => {
339          const remoteAddress = extractRemoteAddress(req);
340          const currentPathname = url.parse(req.url).pathname;
341          log.info(`Got an incoming screen bradcasting request from ${remoteAddress} ` +
342            `(${req.headers['user-agent'] || 'User Agent unknown'}) at ${currentPathname}`);
343
344          if (pathname && currentPathname !== pathname) {
345            log.info('Rejecting the broadcast request since it does not match the given pathname');
346            res.writeHead(404, {
347              Connection: 'close',
348              'Content-Type': 'text/plain; charset=utf-8',
349            });
350            res.write(`'${currentPathname}' did not match any known endpoints`);
351            res.end();
352            return;
353          }
354
355          log.info('Starting MJPEG broadcast');
356          res.writeHead(200, {
357            'Cache-Control': 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
358            Pragma: 'no-cache',
359            Connection: 'close',
360            'Content-Type': `multipart/x-mixed-replace; boundary=${BOUNDARY_STRING}`
361          });
362
363          mjpegSocket.pipe(res);
364        });
365        mjpegServer.on('error', (e) => {
366          log.warn(e);
367          reject(e);
368        });
369        mjpegServer.on('close', () => {
370          log.debug(`MJPEG server at http://${host}:${port} has been closed`);
371        });
372        mjpegServer.on('listening', () => {
373          log.info(`Successfully started MJPEG server at http://${host}:${port}`);
374          resolve();
375        });
376        mjpegServer.listen(port, host);
377      });
378      mjpegSocket.on('error', (e) => {
379        log.error(e);
380        reject(e);
381      });
382    }).timeout(STREAMING_STARTUP_TIMEOUT_MS,
383      `Cannot connect to the streaming server within ${STREAMING_STARTUP_TIMEOUT_MS}ms`);
384  } catch (e) {
385    if (deviceStreamingProc.kill(0)) {
386      deviceStreamingProc.kill();
387    }
388    if (gstreamerPipeline.isRunning) {
389      await gstreamerPipeline.stop();
390    }
391    if (mjpegSocket) {
392      mjpegSocket.destroy();
393    }
394    if (mjpegServer && mjpegServer.listening) {
395      mjpegServer.close();
396    }
397    throw e;
398  }
399
400  this._screenStreamingProps = {
401    deviceStreamingProc,
402    gstreamerPipeline,
403    mjpegSocket,
404    mjpegServer,
405  };
406};
407
408/**
409 * Stop screen streaming.
410 * If no screen streaming server has been started then nothing is done.
411 */
412commands.mobileStopScreenStreaming = async function mobileStopScreenStreaming (/* options = {} */) {
413  if (_.isEmpty(this._screenStreamingProps)) {
414    if (!_.isUndefined(this._screenStreamingProps)) {
415      log.debug(`Screen streaming is not running. There is nothing to stop`);
416    }
417    return;
418  }
419
420  const {
421    deviceStreamingProc,
422    gstreamerPipeline,
423    mjpegSocket,
424    mjpegServer,
425  } = this._screenStreamingProps;
426
427  try {
428    mjpegSocket.end();
429    if (mjpegServer.listening) {
430      mjpegServer.close();
431    }
432    if (deviceStreamingProc.kill(0)) {
433      deviceStreamingProc.kill('SIGINT');
434    }
435    if (gstreamerPipeline.isRunning) {
436      try {
437        await gstreamerPipeline.stop('SIGINT');
438      } catch (e) {
439        log.warn(e);
440        try {
441          await gstreamerPipeline.stop('SIGKILL');
442        } catch (e1) {
443          log.error(e1);
444        }
445      }
446    }
447    log.info(`Successfully terminated the screen streaming MJPEG server`);
448  } finally {
449    this._screenStreamingProps = null;
450  }
451};
452
453
454export default commands;
455
Full Screen

Accelerate Your Automation Test Cycles With LambdaTest

Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.

Try LambdaTest

Run JavaScript Tests on LambdaTest Cloud Grid

Execute automation tests with Appium Android Driver on a cloud-based Grid of 3000+ real browsers and operating systems for both web and mobile applications.

Test now for Free
LambdaTestX

We use cookies to give you the best experience. Cookies help to provide a more personalized experience and relevant advertising for you, and web analytics for us. Learn More in our Cookies policy, Privacy & Terms of service

Allow Cookie
Sarah

I hope you find the best code examples for your project.

If you want to accelerate automated browser testing, try LambdaTest. Your first 100 automation testing minutes are FREE.

Sarah Elson (Product & Growth Lead)