diff --git a/lib/units/device/index.js b/lib/units/device/index.js index ff748990..d2a83b91 100644 --- a/lib/units/device/index.js +++ b/lib/units/device/index.js @@ -17,7 +17,8 @@ module.exports = function(options) { log.info('Preparing device') return syrup.serial() .dependency(require('./plugins/solo')) - .dependency(require('./plugins/screen')) + .dependency(require('./plugins/screen/stream')) + .dependency(require('./plugins/screen/capture')) .dependency(require('./plugins/service')) .dependency(require('./plugins/display')) .dependency(require('./plugins/browser')) diff --git a/lib/units/device/plugins/display.js b/lib/units/device/plugins/display.js index ec31394d..fc89cc5d 100644 --- a/lib/units/device/plugins/display.js +++ b/lib/units/device/plugins/display.js @@ -1,25 +1,71 @@ +var util = require('util') + var syrup = require('stf-syrup') +var EventEmitter = require('eventemitter3').EventEmitter var logger = require('../../../util/logger') +var streamutil = require('../../../util/streamutil') module.exports = syrup.serial() + .dependency(require('../support/adb')) + .dependency(require('../resources/minicap')) .dependency(require('./service')) - .dependency(require('./screen')) - .define(function(options, service, screen) { + .dependency(require('./screen/options')) + .define(function(options, adb, minicap, service, screenOptions) { var log = logger.createLogger('device:plugins:display') - function fetch() { - log.info('Fetching display info') - return service.getDisplay(0) - .catch(function() { - log.info('Falling back to screen API') - return screen.info(0) - }) - .then(function(display) { - display.url = screen.publicUrl - return display + function Display(id, properties) { + this.id = id + this.properties = properties + } + + util.inherits(Display, EventEmitter) + + Display.prototype.updateRotation = function(newRotation) { + log.info('Rotation changed to %d', newRotation) + this.properties.rotation = newRotation + this.emit('rotationChange', newRotation) + } + + function infoFromMinicap(id) { + return minicap.run(util.format('-d %d -i', id)) + .then(streamutil.readAll) + .then(function(out) { + var match + if ((match = /^ERROR: (.*)$/.exec(out))) { + throw new Error(match[1]) + } + + try { + return JSON.parse(out) + } + catch (e) { + throw new Error(out.toString()) + } }) } - return fetch() + function infoFromService(id) { + return service.getDisplay(id) + } + + function readInfo(id) { + log.info('Reading display info') + return infoFromService(id) + .catch(function() { + return infoFromMinicap(id) + }) + .then(function(properties) { + properties.url = screenOptions.publicUrl + return new Display(id, properties) + }) + } + + return readInfo(0).then(function(display) { + service.on('rotationChange', function(data) { + display.updateRotation(data.rotation) + }) + + return display + }) }) diff --git a/lib/units/device/plugins/identity.js b/lib/units/device/plugins/identity.js index c1818a2b..ca77d732 100644 --- a/lib/units/device/plugins/identity.js +++ b/lib/units/device/plugins/identity.js @@ -13,7 +13,7 @@ module.exports = syrup.serial() function solve() { log.info('Solving identity') var identity = devutil.makeIdentity(options.serial, properties) - identity.display = display + identity.display = display.properties identity.phone = phone return identity } diff --git a/lib/units/device/plugins/screen.js b/lib/units/device/plugins/screen.js deleted file mode 100644 index 3af44a3d..00000000 --- a/lib/units/device/plugins/screen.js +++ /dev/null @@ -1,195 +0,0 @@ -var util = require('util') -var path = require('path') -var http = require('http') - -var Promise = require('bluebird') -var syrup = require('stf-syrup') -var httpProxy = require('http-proxy') -var adbkit = require('adbkit') -var _ = require('lodash') - -var logger = require('../../../util/logger') -var lifecycle = require('../../../util/lifecycle') -var streamutil = require('../../../util/streamutil') -var wire = require('../../../wire') -var wireutil = require('../../../wire/util') - -module.exports = syrup.serial() - .dependency(require('../support/adb')) - .dependency(require('../support/router')) - .dependency(require('../support/push')) - .dependency(require('../support/storage')) - .dependency(require('../resources/minicap')) - .define(function(options, adb, router, push, storage, minicap) { - var log = logger.createLogger('device:plugins:screen') - var plugin = Object.create(null) - - plugin.devicePort = 9002 - - plugin.privatePort = options.ports.pop() - plugin.privateUrl = util.format( - 'ws://127.0.0.1:%s' - , plugin.privatePort - ) - - plugin.publicPort = options.ports.pop() - plugin.publicUrl = _.template(options.screenWsUrlPattern)({ - publicIp: options.publicIp - , publicPort: plugin.publicPort - , privatePort: plugin.privatePort - , serial: options.serial - }) - - function run(cmd) { - return adb.shell(options.serial, util.format( - 'LD_LIBRARY_PATH=%s exec %s %s' - , path.dirname(minicap.lib) - , minicap.bin - , cmd - )) - } - - function startService() { - log.info('Launching screen service') - return run(util.format('-p %d', plugin.devicePort)) - .timeout(10000) - .then(function(out) { - lifecycle.share('Screen shell', out) - streamutil.talk(log, 'Screen shell says: "%s"', out) - }) - } - - function forwardService() { - log.info('Opening WebSocket service on port %d', plugin.privatePort) - return adb.forward( - options.serial - , util.format('tcp:%d', plugin.privatePort) - , util.format('tcp:%d', plugin.devicePort) - ) - .timeout(10000) - } - - function startProxy() { - log.info('Starting WebSocket proxy on %s', plugin.publicUrl) - - var resolver = Promise.defer() - - function resolve() { - lifecycle.share('Proxy server', proxyServer, { - end: false - }) - resolver.resolve() - } - - function reject(err) { - resolver.reject(err) - } - - function ignore() { - // No-op - } - - var proxy = httpProxy.createProxyServer({ - target: plugin.privateUrl - , ws: true - , xfwd: false - }) - - proxy.on('error', ignore) - - var proxyServer = http.createServer() - - proxyServer.on('listening', resolve) - proxyServer.on('error', reject) - - proxyServer.on('request', function(req, res) { - proxy.web(req, res) - }) - - proxyServer.on('upgrade', function(req, socket, head) { - proxy.ws(req, socket, head) - }) - - proxyServer.listen(plugin.publicPort) - - return resolver.promise.finally(function() { - proxyServer.removeListener('listening', resolve) - proxyServer.removeListener('error', reject) - }) - } - - plugin.capture = function() { - log.info('Capturing screenshot') - - var file = util.format('/data/local/tmp/minicap_%d.jpg', Date.now()) - return run(util.format('-s >%s', file)) - .then(adbkit.util.readAll) - .then(function() { - return adb.stat(options.serial, file) - }) - .then(function(stats) { - if (stats.size === 0) { - throw new Error('Empty screenshot; possibly secure screen?') - } - - return adb.pull(options.serial, file) - .then(function(transfer) { - return storage.store('image', transfer, { - filename: util.format('%s.jpg', options.serial) - , contentType: 'image/jpeg' - , knownLength: stats.size - }) - }) - }) - .finally(function() { - return adb.shell(options.serial, ['rm', '-f', file]) - .then(adbkit.util.readAll) - }) - } - - router.on(wire.ScreenCaptureMessage, function(channel) { - var reply = wireutil.reply(options.serial) - plugin.capture() - .then(function(file) { - push.send([ - channel - , reply.okay('success', file) - ]) - }) - .catch(function(err) { - log.error('Screen capture failed', err.stack) - push.send([ - channel - , reply.fail(err.message) - ]) - }) - }) - - return startService() - .then(forwardService) - .then(function() { - if (!options.disableScreenPublicProxy) { - return startProxy() - } - }) - .then(function() { - plugin.info = function(id) { - return run(util.format('-d %d -i', id)) - .then(streamutil.readAll) - .then(function(out) { - var match - if ((match = /^ERROR: (.*)$/.exec(out))) { - throw new Error(match[1]) - } - - try { - return JSON.parse(out) - } - catch (e) { - throw new Error(out.toString()) - } - }) - } - }) - .return(plugin) - }) diff --git a/lib/units/device/plugins/screen/capture.js b/lib/units/device/plugins/screen/capture.js new file mode 100644 index 00000000..4a4bb4ca --- /dev/null +++ b/lib/units/device/plugins/screen/capture.js @@ -0,0 +1,81 @@ +var util = require('util') + +var syrup = require('stf-syrup') +var adbkit = require('adbkit') + +var logger = require('../../../../util/logger') +var wire = require('../../../../wire') +var wireutil = require('../../../../wire/util') + +/*jshint maxlen:90*/ +module.exports = syrup.serial() + .dependency(require('../../support/adb')) + .dependency(require('../../support/router')) + .dependency(require('../../support/push')) + .dependency(require('../../support/storage')) + .dependency(require('../../resources/minicap')) + .dependency(require('../display')) + .define(function(options, adb, router, push, storage, minicap, display) { + var log = logger.createLogger('device:plugins:screen:capture') + var plugin = Object.create(null) + + function projectionFormat() { + return util.format( + '%dx%d@%dx%d/%d' + , display.width + , display.height + , display.width + , display.height + , display.rotation + ) + } + + plugin.capture = function() { + log.info('Capturing screenshot') + + var file = util.format('/data/local/tmp/minicap_%d.jpg', Date.now()) + return minicap.run(util.format('-P %s -s >%s', projectionFormat(), file)) + .then(adbkit.util.readAll) + .then(function() { + return adb.stat(options.serial, file) + }) + .then(function(stats) { + if (stats.size === 0) { + throw new Error('Empty screenshot; possibly secure screen?') + } + + return adb.pull(options.serial, file) + .then(function(transfer) { + return storage.store('image', transfer, { + filename: util.format('%s.jpg', options.serial) + , contentType: 'image/jpeg' + , knownLength: stats.size + }) + }) + }) + .finally(function() { + return adb.shell(options.serial, ['rm', '-f', file]) + .then(adbkit.util.readAll) + }) + } + + router.on(wire.ScreenCaptureMessage, function(channel) { + var reply = wireutil.reply(options.serial) + plugin.capture() + .then(function(file) { + push.send([ + channel + , reply.okay('success', file) + ]) + }) + .catch(function(err) { + log.error('Screen capture failed', err.stack) + push.send([ + channel + , reply.fail(err.message) + ]) + }) + }) + + return plugin + }) diff --git a/lib/units/device/plugins/screen/options.js b/lib/units/device/plugins/screen/options.js new file mode 100644 index 00000000..fcdcff30 --- /dev/null +++ b/lib/units/device/plugins/screen/options.js @@ -0,0 +1,17 @@ +var syrup = require('stf-syrup') +var _ = require('lodash') + +module.exports = syrup.serial() + .define(function(options) { + var plugin = Object.create(null) + + plugin.devicePort = 9002 + plugin.publicPort = options.ports.pop() + plugin.publicUrl = _.template(options.screenWsUrlPattern)({ + publicIp: options.publicIp + , publicPort: plugin.publicPort + , serial: options.serial + }) + + return plugin + }) diff --git a/lib/units/device/plugins/screen/stream.js b/lib/units/device/plugins/screen/stream.js new file mode 100644 index 00000000..be1eb02b --- /dev/null +++ b/lib/units/device/plugins/screen/stream.js @@ -0,0 +1,394 @@ +var util = require('util') + +var Promise = require('bluebird') +var syrup = require('stf-syrup') +var WebSocketServer = require('ws').Server +var uuid = require('node-uuid') +var EventEmitter = require('eventemitter3').EventEmitter +var split = require('split') +var adbkit = require('adbkit') + +var logger = require('../../../../util/logger') +var lifecycle = require('../../../../util/lifecycle') +var bannerutil = require('./util/banner') +var FrameParser = require('./util/frameparser') +var FrameConfig = require('./util/frameconfig') +var BroadcastSet = require('./util/broadcastset') +var StateQueue = require('./util/statequeue') + +module.exports = syrup.serial() + .dependency(require('../../support/adb')) + .dependency(require('../../resources/minicap')) + .dependency(require('../display')) + .dependency(require('./options')) + .define(function(options, adb, minicap, display, screenOptions) { + var log = logger.createLogger('device:plugins:screen:stream') + var plugin = Object.create(null) + + function FrameProducer(config) { + this.actionQueue = [] + this.runningState = FrameProducer.STATE_STOPPED + this.desiredState = new StateQueue() + this.output = null + this.socket = null + this.banner = null + this.frameConfig = config + } + + util.inherits(FrameProducer, EventEmitter) + + FrameProducer.STATE_STOPPED = 1 + FrameProducer.STATE_STARTING = 2 + FrameProducer.STATE_STARTED = 3 + FrameProducer.STATE_STOPPING = 4 + + FrameProducer.prototype._ensureState = function() { + switch (this.runningState) { + case FrameProducer.STATE_STARTING: + case FrameProducer.STATE_STOPPING: + // Just wait. + break + case FrameProducer.STATE_STOPPED: + if (this.desiredState.next() === FrameProducer.STATE_STARTED) { + this.runningState = FrameProducer.STATE_STARTING + this._startService().bind(this) + .then(function(out) { + this.output = out + return this._readOutput(out) + }) + .then(function() { + return this._connectService() + }) + .then(function(socket) { + this.socket = socket + return this._readBanner(socket) + }) + .then(function(banner) { + this.banner = banner + return this._readFrames() + }) + .then(function() { + this.runningState = FrameProducer.STATE_STARTED + this.emit('start') + }) + .catch(function(err) { + this.runningState = FrameProducer.STATE_STOPPED + this.emit('error', err) + }) + .finally(function() { + this._ensureState() + }) + } + break + case FrameProducer.STATE_STARTED: + if (this.desiredState.next() === FrameProducer.STATE_STOPPED) { + this.runningState = FrameProducer.STATE_STOPPING + this._disconnectService().bind(this) + .timeout(2000) + .then(function() { + return this._stopService().timeout(10000) + }) + .then(function() { + this.runningState = FrameProducer.STATE_STOPPED + this.emit('stop') + }) + .catch(function(err) { + // In practice we _should_ never get here due to _stopService() + // being quite aggressive. But if we do, well... assume it + // stopped anyway for now. + this.runningState = FrameProducer.STATE_STOPPED + this.emit('error', err) + this.emit('stop') + }) + .finally(function() { + this.output = null + this.socket = null + this.banner = null + this._ensureState() + }) + } + break + } + } + + FrameProducer.prototype.start = function() { + log.info('Requesting frame producer to start') + this.desiredState.push(FrameProducer.STATE_STARTED) + this._ensureState() + } + + FrameProducer.prototype.stop = function() { + log.info('Requesting frame producer to stop') + this.desiredState.push(FrameProducer.STATE_STOPPED) + this._ensureState() + } + + FrameProducer.prototype.updateRotation = function(rotation) { + if (this.frameConfig.rotation === rotation) { + log.info('Keeping %d as current frame producer rotation', rotation) + return + } + + log.info('Setting frame producer rotation to %d', rotation) + this.frameConfig.rotation = rotation + this._configChanged() + } + + FrameProducer.prototype.updateProjection = function(width, height) { + if (this.frameConfig.virtualWidth === width && + this.frameConfig.virtualHeight === height) { + log.info( + 'Keeping %dx%d as current frame producer projection', width, height) + return + } + + log.info('Setting frame producer projection to %dx%d', width, height) + this.frameConfig.virtualWidth = width + this.frameConfig.virtualHeight = height + this._configChanged() + } + + FrameProducer.prototype._configChanged = function() { + switch (this.runningState) { + case FrameProducer.STATE_STARTED: + case FrameProducer.STATE_STARTING: + this.desiredState.push(FrameProducer.STATE_STOPPED) + this.desiredState.push(FrameProducer.STATE_STARTED) + this._ensureState() + break + } + } + + FrameProducer.prototype._startService = function() { + log.info('Launching screen service') + return minicap.run(util.format('-P %s', this.frameConfig.toString())) + .timeout(10000) + } + + FrameProducer.prototype._readOutput = function(out) { + out.pipe(split()).on('data', function(line) { + var trimmed = line.toString().trim() + + if (trimmed === '') { + return + } + + if (/ERROR/.test(line)) { + log.fatal('minicap error: "%s"', line) + return lifecycle.fatal() + } + + log.info('minicap says: "%s"', line) + }) + } + + FrameProducer.prototype._connectService = function() { + function tryConnect(times, delay) { + return adb.openLocal(options.serial, 'localabstract:minicap') + .timeout(10000) + .then(function(out) { + return out + }) + .catch(function(err) { + if (/closed/.test(err.message) && times > 1) { + return Promise.delay(delay) + .then(function() { + return tryConnect(--times, delay * 2) + }) + } + return Promise.reject(err) + }) + } + log.info('Connecting to minicap service') + return tryConnect(5, 100) + } + + FrameProducer.prototype._disconnectService = function() { + log.info('Disconnecting from minicap service') + + var socket = this.socket + + var endListener + return new Promise(function(resolve/*, reject*/) { + socket.on('end', endListener = function() { + resolve(true) + }) + + socket.end() + }) + .finally(function() { + socket.removeListener('end', endListener) + }) + } + + FrameProducer.prototype._stopService = function() { + log.info('Stopping minicap service') + + var pid = this.banner ? this.banner.pid : -1 + var output = this.output + + function waitForEnd() { + var endListener + return new Promise(function(resolve/*, reject*/) { + output.on('end', endListener = function() { + resolve(true) + }) + }) + .finally(function() { + output.removeListener('end', endListener) + }) + } + + function kindKill() { + if (pid <= 0) { + return Promise.reject(new Error('Minicap service pid is unknown')) + } + + log.info('Sending SIGTERM to minicap') + return Promise.all([ + waitForEnd() + , adb.shell(options.serial, ['kill', pid]) + .then(adbkit.util.readAll) + .timeout(2000) + .return(true) + ]) + } + + function forceKill() { + if (pid <= 0) { + return Promise.reject(new Error('Minicap service pid is unknown')) + } + + log.info('Sending SIGKILL to minicap') + return Promise.all([ + waitForEnd() + , adb.shell(options.serial, ['kill', '-9', pid]) + .then(adbkit.util.readAll) + .timeout(2000) + .return(true) + ]) + } + + function forceEnd() { + log.info('Ending minicap I/O as a last resort') + output.end() + return Promise.resolve(true) + } + + return kindKill() + .catch(Promise.TimeoutError, forceKill) + .catch(forceEnd) + } + + FrameProducer.prototype._readBanner = function(out) { + log.info('Reading minicap banner') + return bannerutil.read(out).timeout(2000) + } + + FrameProducer.prototype._readFrames = function() { + var parser = this.socket.pipe(new FrameParser()) + var emit = this.emit.bind(this) + + function tryRead() { + for (var frame; (frame = parser.read());) { + emit('frame', frame) + } + } + + tryRead() + parser.on('readable', tryRead) + } + + function createServer() { + log.info('Starting WebSocket server on port %d', screenOptions.publicPort) + + var wss = new WebSocketServer({ + port: screenOptions.publicPort + , perMessageDeflate: false + }) + + var listeningListener, errorListener + return new Promise(function(resolve, reject) { + listeningListener = function() { + return resolve(wss) + } + + errorListener = function(err) { + return reject(err) + } + + wss.on('listening', listeningListener) + wss.on('error', errorListener) + }) + .finally(function() { + wss.removeListener('listening', listeningListener) + wss.removeListener('error', errorListener) + }) + } + + return createServer() + .then(function(wss) { + var broadcastSet = new BroadcastSet() + var frameProducer = new FrameProducer( + new FrameConfig(display.properties, display.properties)) + + broadcastSet.on('nonempty', function() { + frameProducer.start() + }) + + broadcastSet.on('empty', function() { + frameProducer.stop() + }) + + display.on('rotationChange', function(newRotation) { + frameProducer.updateRotation(newRotation) + }) + + frameProducer.on('frame', function(frame) { + broadcastSet.each(function(ws) { + ws.send(frame, { + binary: true + }) + }) + }) + + frameProducer.on('error', function(err) { + log.fatal('Frame producer had an error', err.stack) + lifecycle.fatal() + }) + + wss.on('connection', function(ws) { + var id = uuid.v4() + + ws.on('message', function(data) { + var match + if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) { + switch (match[2] || match[1]) { + case 'on': + broadcastSet.insert(id, ws) + break + case 'off': + broadcastSet.remove(id) + break + case 'size': + frameProducer.updateProjection(+match[3], +match[4]) + break + } + } + }) + + ws.on('close', function() { + broadcastSet.remove(id) + }) + }) + + lifecycle.observe(function() { + wss.close() + }) + + lifecycle.observe(function() { + frameProducer.stop() + }) + }) + .return(plugin) + }) diff --git a/lib/units/device/plugins/screen/util/banner.js b/lib/units/device/plugins/screen/util/banner.js new file mode 100644 index 00000000..9d8ce27b --- /dev/null +++ b/lib/units/device/plugins/screen/util/banner.js @@ -0,0 +1,108 @@ +var Promise = require('bluebird') + +module.exports.read = function parseBanner(out) { + var tryRead + + return new Promise(function(resolve, reject) { + var readBannerBytes = 0 + var needBannerBytes = 2 + + var banner = out.banner = { + version: 0 + , length: 0 + , pid: 0 + , realWidth: 0 + , realHeight: 0 + , virtualWidth: 0 + , virtualHeight: 0 + , orientation: 0 + , quirks: 0 + } + + tryRead = function() { + for (var chunk; (chunk = out.read(needBannerBytes - readBannerBytes));) { + for (var cursor = 0, len = chunk.length; cursor < len;) { + if (readBannerBytes < needBannerBytes) { + switch (readBannerBytes) { + case 0: + // version + banner.version = chunk[cursor] + break + case 1: + // length + banner.length = needBannerBytes = chunk[cursor] + break + case 2: + case 3: + case 4: + case 5: + // pid + banner.pid += + (chunk[cursor] << ((readBannerBytes - 2) * 8)) >>> 0 + break + case 6: + case 7: + case 8: + case 9: + // real width + banner.realWidth += + (chunk[cursor] << ((readBannerBytes - 6) * 8)) >>> 0 + break + case 10: + case 11: + case 12: + case 13: + // real height + banner.realHeight += + (chunk[cursor] << ((readBannerBytes - 10) * 8)) >>> 0 + break + case 14: + case 15: + case 16: + case 17: + // virtual width + banner.virtualWidth += + (chunk[cursor] << ((readBannerBytes - 14) * 8)) >>> 0 + break + case 18: + case 19: + case 20: + case 21: + // virtual height + banner.virtualHeight += + (chunk[cursor] << ((readBannerBytes - 18) * 8)) >>> 0 + break + case 22: + // orientation + banner.orientation += chunk[cursor] * 90 + break + case 23: + // quirks + banner.quirks = chunk[cursor] + break + } + + cursor += 1 + readBannerBytes += 1 + + if (readBannerBytes === needBannerBytes) { + return resolve(banner) + } + } + else { + reject(new Error( + 'Supposedly impossible error parsing banner' + )) + } + } + } + } + + tryRead() + + out.on('readable', tryRead) + }) + .finally(function() { + out.removeListener('readable', tryRead) + }) +} diff --git a/lib/units/device/plugins/screen/util/broadcastset.js b/lib/units/device/plugins/screen/util/broadcastset.js new file mode 100644 index 00000000..0eef8626 --- /dev/null +++ b/lib/units/device/plugins/screen/util/broadcastset.js @@ -0,0 +1,40 @@ +var util = require('util') + +var EventEmitter = require('eventemitter3').EventEmitter + +function BroadcastSet() { + this.set = Object.create(null) + this.count = 0 +} + +util.inherits(BroadcastSet, EventEmitter) + +BroadcastSet.prototype.insert = function(id, ws) { + if (!(id in this.set)) { + this.set[id] = ws + this.count += 1 + this.emit('insert', id) + if (this.count === 1) { + this.emit('nonempty') + } + } +} + +BroadcastSet.prototype.remove = function(id) { + if (id in this.set) { + delete this.set[id] + this.count -= 1 + this.emit('remove', id) + if (this.count === 0) { + this.emit('empty') + } + } +} + +BroadcastSet.prototype.each = function(fn) { + return Object.keys(this.set).forEach(function(id) { + return fn(this.set[id]) + }, this) +} + +module.exports = BroadcastSet diff --git a/lib/units/device/plugins/screen/util/frameconfig.js b/lib/units/device/plugins/screen/util/frameconfig.js new file mode 100644 index 00000000..3a9d8042 --- /dev/null +++ b/lib/units/device/plugins/screen/util/frameconfig.js @@ -0,0 +1,22 @@ +var util = require('util') + +function FrameConfig(real, virtual) { + this.realWidth = real.width + this.realHeight = real.height + this.virtualWidth = virtual.width + this.virtualHeight = virtual.height + this.rotation = virtual.rotation +} + +FrameConfig.prototype.toString = function() { + return util.format( + '%dx%d@%dx%d/%d' + , this.realWidth + , this.realHeight + , this.virtualWidth + , this.virtualHeight + , this.rotation + ) +} + +module.exports = FrameConfig diff --git a/lib/units/device/plugins/screen/util/frameparser.js b/lib/units/device/plugins/screen/util/frameparser.js new file mode 100644 index 00000000..7260905f --- /dev/null +++ b/lib/units/device/plugins/screen/util/frameparser.js @@ -0,0 +1,56 @@ +var stream = require('stream') +var util = require('util') + +function FrameParser() { + this.readFrameBytes = 0 + this.frameBodyLength = 0 + this.frameBody = new Buffer(0) + stream.Transform.call(this) +} + +util.inherits(FrameParser, stream.Transform) + +FrameParser.prototype._transform = function(chunk, encoding, done) { + var cursor, len, bytesLeft + + for (cursor = 0, len = chunk.length; cursor < len;) { + if (this.readFrameBytes < 4) { + this.frameBodyLength += + (chunk[cursor] << (this.readFrameBytes * 8)) >>> 0 + cursor += 1 + this.readFrameBytes += 1 + } + else { + bytesLeft = len - cursor + + if (bytesLeft >= this.frameBodyLength) { + this.frameBody = Buffer.concat([ + this.frameBody + , chunk.slice(cursor, cursor + this.frameBodyLength) + ]) + + this.push(this.frameBody) + + cursor += this.frameBodyLength + this.frameBodyLength = this.readFrameBytes = 0 + this.frameBody = new Buffer(0) + } + else { + // @todo Consider/benchmark continuation frames to prevent + // potential Buffer thrashing. + this.frameBody = Buffer.concat([ + this.frameBody + , chunk.slice(cursor, len) + ]) + + this.frameBodyLength -= bytesLeft + this.readFrameBytes += bytesLeft + cursor = len + } + } + } + + return done() +} + +module.exports = FrameParser diff --git a/lib/units/device/plugins/screen/util/statequeue.js b/lib/units/device/plugins/screen/util/statequeue.js new file mode 100644 index 00000000..da3a8337 --- /dev/null +++ b/lib/units/device/plugins/screen/util/statequeue.js @@ -0,0 +1,26 @@ +function StateQueue() { + this.queue = [] +} + +StateQueue.prototype.next = function() { + return this.queue.shift() +} + +StateQueue.prototype.push = function(state) { + var found = false + + // Not super efficient, but this shouldn't be running all the time anyway. + for (var i = 0, l = this.queue.length; i < l; ++i) { + if (this.queue[i] === state) { + this.queue.splice(i + 1) + found = true + break + } + } + + if (!found) { + this.queue.push(state) + } +} + +module.exports = StateQueue diff --git a/lib/units/device/resources/minicap.js b/lib/units/device/resources/minicap.js index 30109a99..041bd058 100644 --- a/lib/units/device/resources/minicap.js +++ b/lib/units/device/resources/minicap.js @@ -1,4 +1,5 @@ var util = require('util') +var path = require('path') var Promise = require('bluebird') var syrup = require('stf-syrup') @@ -87,6 +88,14 @@ module.exports = syrup.serial() return { bin: resources.bin.dest , lib: resources.lib.dest + , run: function(cmd) { + return adb.shell(options.serial, util.format( + 'LD_LIBRARY_PATH=%s exec %s %s' + , path.dirname(resources.lib.dest) + , resources.bin.dest + , cmd + )) + } } }) }) diff --git a/res/app/components/stf/screen/rotator-test.js b/res/app/components/stf/screen/rotator-test.js new file mode 100644 index 00000000..2807848f --- /dev/null +++ b/res/app/components/stf/screen/rotator-test.js @@ -0,0 +1,63 @@ +var rotator = require('./rotator') + +var tests = [ + [0, 0, +0] +, [0, 90, -90] +, [0, 180, -180] +, [0, 270, +90] +, [90, 0, +90] +, [90, 90, +0] +, [90, 180, -90] +, [90, 270, +180] +, [180, 0, +180] +, [180, 90, +90] +, [180, 180, +0] +, [180, 270, -90] +, [270, 0, -90] +, [270, 90, -180] +, [270, 180, +90] +, [270, 270, +0] +, [360, 0, +0] +, [360, 90, -90] +, [360, 180, -180] +, [360, 270, +90] +, [-90, 0, -90] +, [-90, 90, -180] +, [-90, 180, +90] +, [-90, 270, 0] +, [-180, 0, +180] +, [-180, 90, +90] +, [-180, 180, +0] +, [-180, 270, -90] +, [-270, 0, +90] +, [-270, 90, 0] +, [-270, 180, -90] +, [-270, 270, +180] +, [720, 0, +0] +, [720, 90, -90] +, [720, 180, -180] +, [720, 270, +90] +, [450, 0, +90] +, [450, 90, +0] +, [450, 180, -90] +, [450, 270, +180] +, [540, 0, +180] +, [540, 90, +90] +, [540, 180, +0] +, [540, 270, -90] +, [630, 0, -90] +, [630, 90, -180] +, [630, 180, +90] +, [630, 270, +0] +] + +tests.forEach(function(values) { + var msg = values[0] + ' -> ' + values[1] + ' should be ' + values[2] + + if (rotator(values[0], values[1]) === values[2]) { + console.log('pass: ' + msg) + } + else { + console.error('FAIL: ' + msg) + } +}) diff --git a/res/app/components/stf/screen/rotator.js b/res/app/components/stf/screen/rotator.js new file mode 100644 index 00000000..03546e02 --- /dev/null +++ b/res/app/components/stf/screen/rotator.js @@ -0,0 +1,33 @@ +var mapping = { + 0: { + 0: 0 + , 90: -90 + , 180: -180 + , 270: 90 + } +, 90: { + 0: 90 + , 90: 0 + , 180: -90 + , 270: 180 + } +, 180: { + 0: 180 + , 90: 90 + , 180: 0 + , 270: -90 + } +, 270: { + 0: -90 + , 90: -180 + , 180: 90 + , 270: 0 + } +} + +module.exports = function rotator(oldRotation, newRotation) { + var r1 = oldRotation < 0 ? 360 + oldRotation % 360 : oldRotation % 360 + , r2 = newRotation < 0 ? 360 + newRotation % 360 : newRotation % 360 + + return mapping[r1][r2] +} diff --git a/res/app/components/stf/screen/screen-directive.js b/res/app/components/stf/screen/screen-directive.js index b19d05ad..8d21767e 100644 --- a/res/app/components/stf/screen/screen-directive.js +++ b/res/app/components/stf/screen/screen-directive.js @@ -1,4 +1,5 @@ var _ = require('lodash') +var rotator = require('./rotator') module.exports = function DeviceScreenDirective( $document @@ -47,182 +48,6 @@ module.exports = function DeviceScreenDirective( * This section should deal with updating the screen ONLY. */ ;(function() { - var canvas = element.find('canvas')[0] - , g = canvas.getContext('2d') - - var devicePixelRatio = window.devicePixelRatio || 1 - , backingStoreRatio = g.webkitBackingStorePixelRatio || - g.mozBackingStorePixelRatio || - g.msBackingStorePixelRatio || - g.oBackingStorePixelRatio || - g.backingStorePixelRatio || 1 - , frontBackRatio = devicePixelRatio / backingStoreRatio - - var options = { - autoScaleForRetina: true - , density: Math.max(1, Math.min(1.5, devicePixelRatio || 1)) - , minscale: 0.36 - } - - var updating = false - - var cachedScreen = { - rotation: 0 - , bounds: { - x: 0 - , y: 0 - , w: 0 - , h: 0 - } - } - - var cachedImageWidth = 0 - , cachedImageHeight = 0 - - function updateBounds() { - // TODO: element is an object HTMLUnknownElement in IE9 - screen.bounds.w = element[0].offsetWidth - screen.bounds.h = element[0].offsetHeight - - // Developer error, let's try to reduce debug time - if (!screen.bounds.w || !screen.bounds.h) { - throw new Error( - 'Unable to update display size; container must have dimensions' - ) - } - } - - function maybeLoadScreen() { - var size - if (shouldUpdateScreen()) { - switch (screen.rotation) { - case 0: - case 180: - size = adjustBoundedSize( - screen.bounds.w - , screen.bounds.h - ) - break - case 90: - case 270: - size = adjustBoundedSize( - screen.bounds.h - , screen.bounds.w - ) - break - } - updating = true - ws.send('j ' + size.w + ' ' + size.h) - } - } - - function adjustBoundedSize(w, h) { - var sw = w * options.density - , sh = h * options.density - , f - - if (sw < (f = device.display.width * options.minscale)) { - sw *= f / sw - sh *= f / sh - } - - if (sh < (f = device.display.height * options.minscale)) { - sw *= f / sw - sh *= f / sh - } - - return { - w: Math.ceil(sw) - , h: Math.ceil(sh) - } - } - - function shouldUpdateScreen() { - return ( - // NO if we're updating already. - !updating && - // NO if the user has disabled the screen. - scope.$parent.showScreen && - // NO if we're not even using the device anymore. - device.using && - // NO if the page invisible to the user? - !PageVisibilityService.hidden && - // NO if we don't have a connection yet. - ws.readyState === WebSocket.OPEN - // YES otherwise - ) - } - - function hasImageAreaChanged(img) { - return cachedScreen.bounds.w !== screen.bounds.w || - cachedScreen.bounds.h !== screen.bounds.h || - cachedImageWidth !== img.width || - cachedImageHeight !== img.height || - cachedScreen.rotation !== screen.rotation - } - - function updateImageArea(img) { - if (!hasImageAreaChanged(img)) { - return - } - - cachedScreen.bounds.w = screen.bounds.w - cachedScreen.bounds.h = screen.bounds.h - - cachedImageWidth = img.width - cachedImageHeight = img.height - - cachedScreen.rotation = screen.rotation - - if (options.autoScaleForRetina) { - canvas.width = cachedImageWidth * frontBackRatio - canvas.height = cachedImageHeight * frontBackRatio - g.scale(frontBackRatio, frontBackRatio) - } - else { - canvas.width = cachedImageWidth - canvas.height = cachedImageHeight - } - - var projectedSize = scaler.projectedSize( - screen.bounds.w - , screen.bounds.h - , screen.rotation - ) - - canvas.style.width = projectedSize.width + 'px' - canvas.style.height = projectedSize.height + 'px' - - // @todo Make sure that each position is able to rotate smoothly - // to the next one. This current setup doesn't work if rotation - // changes from 180 to 270 (it will do a reverse rotation). - switch (screen.rotation) { - case 0: - canvas.style[cssTransform] = 'rotate(0deg)' - break - case 90: - canvas.style[cssTransform] = 'rotate(-90deg)' - break - case 180: - canvas.style[cssTransform] = 'rotate(-180deg)' - break - case 270: - canvas.style[cssTransform] = 'rotate(90deg)' - break - } - } - - function checkEnabled() { - if (shouldUpdateScreen()) { - updating = false - updateBounds() - maybeLoadScreen() - } - else { - g.clearRect(0, 0, canvas.width, canvas.height) - } - } - function stop() { try { ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null @@ -235,6 +60,8 @@ module.exports = function DeviceScreenDirective( var ws = new WebSocket(device.display.url) ws.binaryType = 'blob' + var cleanupList = [] + ws.onerror = function errorListener() { // @todo Handle } @@ -244,80 +71,252 @@ module.exports = function DeviceScreenDirective( } ws.onopen = function openListener() { - checkEnabled() - } + var canvas = element.find('canvas')[0] + , g = canvas.getContext('2d') - ws.onmessage = function messageListener(message) { - updating = false + function vendorBackingStorePixelRatio(g) { + return g.webkitBackingStorePixelRatio || + g.mozBackingStorePixelRatio || + g.msBackingStorePixelRatio || + g.oBackingStorePixelRatio || + g.backingStorePixelRatio || 1 + } - if (shouldUpdateScreen()) { - screen.rotation = device.display.rotation + var devicePixelRatio = window.devicePixelRatio || 1 + , backingStoreRatio = vendorBackingStorePixelRatio(g) + , frontBackRatio = devicePixelRatio / backingStoreRatio - if (message.data instanceof Blob) { - if (scope.displayError) { - scope.$apply(function () { - scope.displayError = false - }) + var options = { + autoScaleForRetina: true + , density: Math.max(1, Math.min(1.5, devicePixelRatio || 1)) + , minscale: 0.36 + } + + var cachedScreen = { + rotation: 0 + , bounds: { + x: 0 + , y: 0 + , w: 0 + , h: 0 + } + } + + var adjustedBoundSize + + var cachedImageWidth = 0 + , cachedImageHeight = 0 + , cachedEnabled = false + , cssRotation = 0 + + function updateBounds() { + function adjustBoundedSize(w, h) { + var sw = w * options.density + , sh = h * options.density + , f + + if (sw < (f = device.display.width * options.minscale)) { + sw *= f / sw + sh *= f / sh } - var blob = new Blob([message.data], { - type: 'image/jpeg' - }) - - var img = new Image() - - img.onload = function() { - updateImageArea(this) - - g.drawImage(img, 0, 0) - - // Try to forcefully clean everything to get rid of memory - // leaks. - img.onload = img.onerror = null - img.src = BLANK_IMG - img = null - url = null - blob = null + if (sh < (f = device.display.height * options.minscale)) { + sw *= f / sw + sh *= f / sh } - img.onerror = function() { - // Happily ignore. I suppose this shouldn't happen, but - // sometimes it does, presumably when we're loading images - // too quickly. + return { + w: Math.ceil(sw) + , h: Math.ceil(sh) } + } - var url = URL.createObjectURL(blob) - img.src = url + // FIXME: element is an object HTMLUnknownElement in IE9 + var w = screen.bounds.w = element[0].offsetWidth + , h = screen.bounds.h = element[0].offsetHeight + + // Developer error, let's try to reduce debug time + if (!w || !h) { + throw new Error( + 'Unable to read bounds; container must have dimensions' + ) + } + + var newAdjustedBoundSize = (function() { + switch (screen.rotation) { + case 90: + case 270: + return adjustBoundedSize(h, w) + case 0: + case 180: + /* falls through */ + default: + return adjustBoundedSize(w, h) + } + })() + + if (!adjustedBoundSize + || newAdjustedBoundSize.w !== adjustedBoundSize.w + || newAdjustedBoundSize.h !== adjustedBoundSize.h) { + adjustedBoundSize = newAdjustedBoundSize + onScreenInterestAreaChanged() + } + } + + function shouldUpdateScreen() { + return ( + // NO if the user has disabled the screen. + scope.$parent.showScreen && + // NO if we're not even using the device anymore. + device.using && + // NO if the page is not visible (e.g. background tab). + !PageVisibilityService.hidden + // YES otherwise + ) + } + + function checkEnabled() { + var newEnabled = shouldUpdateScreen() + + if (newEnabled === cachedEnabled) { + updateBounds() + } + else if (newEnabled) { + updateBounds() + onScreenInterestGained() } else { - switch (message.data) { - case 'secure_on': - scope.$apply(function () { - scope.displayError = 'secure' - }) - break - } + g.clearRect(0, 0, canvas.width, canvas.height) + onScreenInterestLost() } - // Next please - maybeLoadScreen() + cachedEnabled = newEnabled } - } - // NOTE: instead of fa-pane-resize, a fa-child-pane-resize could be better - scope.$on('fa-pane-resize', _.throttle(updateBounds, 16)) + function onScreenInterestGained() { + ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h) + ws.send('on') + } + + function onScreenInterestAreaChanged() { + ws.send('size ' + adjustedBoundSize.w + 'x' + adjustedBoundSize.h) + } + + function onScreenInterestLost() { + ws.send('off') + } + + ws.onmessage = function messageListener(message) { + function hasImageAreaChanged(img) { + return cachedScreen.bounds.w !== screen.bounds.w || + cachedScreen.bounds.h !== screen.bounds.h || + cachedImageWidth !== img.width || + cachedImageHeight !== img.height || + cachedScreen.rotation !== screen.rotation + } + + function updateImageArea(img) { + if (!hasImageAreaChanged(img)) { + return + } + + cachedImageWidth = img.width + cachedImageHeight = img.height + + if (options.autoScaleForRetina) { + canvas.width = cachedImageWidth * frontBackRatio + canvas.height = cachedImageHeight * frontBackRatio + g.scale(frontBackRatio, frontBackRatio) + } + else { + canvas.width = cachedImageWidth + canvas.height = cachedImageHeight + } + + var projectedSize = scaler.projectedSize( + screen.bounds.w + , screen.bounds.h + , screen.rotation + ) + + cssRotation += rotator(cachedScreen.rotation, screen.rotation) + + canvas.style.width = projectedSize.width + 'px' + canvas.style.height = projectedSize.height + 'px' + canvas.style[cssTransform] = 'rotate(' + cssRotation + 'deg)' + + cachedScreen.bounds.h = screen.bounds.h + cachedScreen.bounds.w = screen.bounds.w + cachedScreen.rotation = screen.rotation + } + + if (shouldUpdateScreen()) { + screen.rotation = device.display.rotation + + if (message.data instanceof Blob) { + if (scope.displayError) { + scope.$apply(function () { + scope.displayError = false + }) + } + + var blob = new Blob([message.data], { + type: 'image/jpeg' + }) + + var img = new Image() + + img.onload = function() { + updateImageArea(this) + + g.drawImage(img, 0, 0) + + // Try to forcefully clean everything to get rid of memory + // leaks. Note that despite this effort, Chrome will still + // leak huge amounts of memory when the developer tools are + // open, probably to save the resources for inspection. When + // the developer tools are closed no memory is leaked. + img.onload = img.onerror = null + img.src = BLANK_IMG + img = null + url = null + blob = null + } + + img.onerror = function() { + // Happily ignore. I suppose this shouldn't happen, but + // sometimes it does, presumably when we're loading images + // too quickly. + } + + var url = URL.createObjectURL(blob) + img.src = url + } + else { + switch (message.data) { + case 'secure_on': + scope.$apply(function () { + scope.displayError = 'secure' + }) + break + } + } + } + } + + // NOTE: instead of fa-pane-resize, a fa-child-pane-resize could be better + cleanupList.push(scope.$on('fa-pane-resize', _.throttle(updateBounds, 1000))) + cleanupList.push(scope.$watch('device.using', checkEnabled)) + cleanupList.push(scope.$on('visibilitychange', checkEnabled)) + cleanupList.push(scope.$watch('$parent.showScreen', checkEnabled)) + } scope.retryLoadingScreen = function () { if (scope.displayError === 'secure') { control.home() } - $timeout(maybeLoadScreen, 3000) } - scope.$watch('device.using', checkEnabled) - scope.$on('visibilitychange', checkEnabled) - scope.$watch('$parent.showScreen', checkEnabled) - scope.$on('guest-portrait', function () { control.rotate(0) }) diff --git a/vendor/minicap/bin/arm64-v8a/minicap b/vendor/minicap/bin/arm64-v8a/minicap index d3b8c127..aa9dac7a 100755 Binary files a/vendor/minicap/bin/arm64-v8a/minicap and b/vendor/minicap/bin/arm64-v8a/minicap differ diff --git a/vendor/minicap/bin/arm64-v8a/minicap-nopie b/vendor/minicap/bin/arm64-v8a/minicap-nopie index d3b8c127..aa9dac7a 100755 Binary files a/vendor/minicap/bin/arm64-v8a/minicap-nopie and b/vendor/minicap/bin/arm64-v8a/minicap-nopie differ diff --git a/vendor/minicap/bin/armeabi-v7a/minicap b/vendor/minicap/bin/armeabi-v7a/minicap index 8436ee4f..ff06f9fc 100755 Binary files a/vendor/minicap/bin/armeabi-v7a/minicap and b/vendor/minicap/bin/armeabi-v7a/minicap differ diff --git a/vendor/minicap/bin/armeabi-v7a/minicap-nopie b/vendor/minicap/bin/armeabi-v7a/minicap-nopie index eb36d5ab..37404382 100755 Binary files a/vendor/minicap/bin/armeabi-v7a/minicap-nopie and b/vendor/minicap/bin/armeabi-v7a/minicap-nopie differ diff --git a/vendor/minicap/bin/x86/minicap b/vendor/minicap/bin/x86/minicap index 6e860ca7..bad590c1 100755 Binary files a/vendor/minicap/bin/x86/minicap and b/vendor/minicap/bin/x86/minicap differ diff --git a/vendor/minicap/bin/x86/minicap-nopie b/vendor/minicap/bin/x86/minicap-nopie index 687f96fb..00caf90e 100755 Binary files a/vendor/minicap/bin/x86/minicap-nopie and b/vendor/minicap/bin/x86/minicap-nopie differ diff --git a/vendor/minicap/shared/android-10/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-10/armeabi-v7a/minicap.so index 90efc2a7..c53c3cc5 100755 Binary files a/vendor/minicap/shared/android-10/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-10/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-14/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-14/armeabi-v7a/minicap.so index 4706a8e1..257f07d2 100755 Binary files a/vendor/minicap/shared/android-14/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-14/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-14/x86/minicap.so b/vendor/minicap/shared/android-14/x86/minicap.so index 1d679c00..bdf5532b 100755 Binary files a/vendor/minicap/shared/android-14/x86/minicap.so and b/vendor/minicap/shared/android-14/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-15/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-15/armeabi-v7a/minicap.so index 78f5f085..e0d6a387 100755 Binary files a/vendor/minicap/shared/android-15/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-15/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-15/x86/minicap.so b/vendor/minicap/shared/android-15/x86/minicap.so index e19ca0a7..e8dcf265 100755 Binary files a/vendor/minicap/shared/android-15/x86/minicap.so and b/vendor/minicap/shared/android-15/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-16/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-16/armeabi-v7a/minicap.so index ea2b2ba9..6ca2bf47 100755 Binary files a/vendor/minicap/shared/android-16/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-16/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-16/x86/minicap.so b/vendor/minicap/shared/android-16/x86/minicap.so index c7776e1f..0fed6617 100755 Binary files a/vendor/minicap/shared/android-16/x86/minicap.so and b/vendor/minicap/shared/android-16/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-17/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-17/armeabi-v7a/minicap.so index eb6f2cd3..915db1ba 100755 Binary files a/vendor/minicap/shared/android-17/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-17/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-17/x86/minicap.so b/vendor/minicap/shared/android-17/x86/minicap.so index 4655a247..6c0c850c 100755 Binary files a/vendor/minicap/shared/android-17/x86/minicap.so and b/vendor/minicap/shared/android-17/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-18/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-18/armeabi-v7a/minicap.so index 0c5aec3c..222e5afe 100755 Binary files a/vendor/minicap/shared/android-18/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-18/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-18/x86/minicap.so b/vendor/minicap/shared/android-18/x86/minicap.so index c54e9db2..1bff4f31 100755 Binary files a/vendor/minicap/shared/android-18/x86/minicap.so and b/vendor/minicap/shared/android-18/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-19/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-19/armeabi-v7a/minicap.so index e4a4bb02..eb4fae82 100755 Binary files a/vendor/minicap/shared/android-19/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-19/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-19/x86/minicap.so b/vendor/minicap/shared/android-19/x86/minicap.so index 3fb4642b..db06375a 100755 Binary files a/vendor/minicap/shared/android-19/x86/minicap.so and b/vendor/minicap/shared/android-19/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-21/arm64-v8a/minicap.so b/vendor/minicap/shared/android-21/arm64-v8a/minicap.so index 160ba22e..7297418b 100755 Binary files a/vendor/minicap/shared/android-21/arm64-v8a/minicap.so and b/vendor/minicap/shared/android-21/arm64-v8a/minicap.so differ diff --git a/vendor/minicap/shared/android-21/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-21/armeabi-v7a/minicap.so index 24b140d2..0bea4543 100755 Binary files a/vendor/minicap/shared/android-21/armeabi-v7a/minicap.so and b/vendor/minicap/shared/android-21/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-21/x86/minicap.so b/vendor/minicap/shared/android-21/x86/minicap.so index 1c97df73..56d0bf88 100755 Binary files a/vendor/minicap/shared/android-21/x86/minicap.so and b/vendor/minicap/shared/android-21/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-21/x86_64/minicap.so b/vendor/minicap/shared/android-21/x86_64/minicap.so index a52f4fce..ffe5c18d 100755 Binary files a/vendor/minicap/shared/android-21/x86_64/minicap.so and b/vendor/minicap/shared/android-21/x86_64/minicap.so differ diff --git a/vendor/minicap/shared/android-22/arm64-v8a/minicap.so b/vendor/minicap/shared/android-22/arm64-v8a/minicap.so new file mode 100755 index 00000000..1d8b7388 Binary files /dev/null and b/vendor/minicap/shared/android-22/arm64-v8a/minicap.so differ diff --git a/vendor/minicap/shared/android-22/armeabi-v7a/minicap.so b/vendor/minicap/shared/android-22/armeabi-v7a/minicap.so new file mode 100755 index 00000000..a753b219 Binary files /dev/null and b/vendor/minicap/shared/android-22/armeabi-v7a/minicap.so differ diff --git a/vendor/minicap/shared/android-22/x86/minicap.so b/vendor/minicap/shared/android-22/x86/minicap.so new file mode 100755 index 00000000..0b11f797 Binary files /dev/null and b/vendor/minicap/shared/android-22/x86/minicap.so differ diff --git a/vendor/minicap/shared/android-22/x86_64/minicap.so b/vendor/minicap/shared/android-22/x86_64/minicap.so new file mode 100755 index 00000000..79f39a78 Binary files /dev/null and b/vendor/minicap/shared/android-22/x86_64/minicap.so differ