mirror of
https://github.com/openstf/stf
synced 2025-10-05 02:29:26 +02:00
Integrate new minicap along with a moderate rewrite. What's currently missing is recovering from socket death.
This commit is contained in:
parent
6fe4f8ae1b
commit
95e9dd0b82
43 changed files with 1138 additions and 438 deletions
|
@ -17,7 +17,8 @@ module.exports = function(options) {
|
||||||
log.info('Preparing device')
|
log.info('Preparing device')
|
||||||
return syrup.serial()
|
return syrup.serial()
|
||||||
.dependency(require('./plugins/solo'))
|
.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/service'))
|
||||||
.dependency(require('./plugins/display'))
|
.dependency(require('./plugins/display'))
|
||||||
.dependency(require('./plugins/browser'))
|
.dependency(require('./plugins/browser'))
|
||||||
|
|
|
@ -1,25 +1,71 @@
|
||||||
|
var util = require('util')
|
||||||
|
|
||||||
var syrup = require('stf-syrup')
|
var syrup = require('stf-syrup')
|
||||||
|
var EventEmitter = require('eventemitter3').EventEmitter
|
||||||
|
|
||||||
var logger = require('../../../util/logger')
|
var logger = require('../../../util/logger')
|
||||||
|
var streamutil = require('../../../util/streamutil')
|
||||||
|
|
||||||
module.exports = syrup.serial()
|
module.exports = syrup.serial()
|
||||||
|
.dependency(require('../support/adb'))
|
||||||
|
.dependency(require('../resources/minicap'))
|
||||||
.dependency(require('./service'))
|
.dependency(require('./service'))
|
||||||
.dependency(require('./screen'))
|
.dependency(require('./screen/options'))
|
||||||
.define(function(options, service, screen) {
|
.define(function(options, adb, minicap, service, screenOptions) {
|
||||||
var log = logger.createLogger('device:plugins:display')
|
var log = logger.createLogger('device:plugins:display')
|
||||||
|
|
||||||
function fetch() {
|
function Display(id, properties) {
|
||||||
log.info('Fetching display info')
|
this.id = id
|
||||||
return service.getDisplay(0)
|
this.properties = properties
|
||||||
.catch(function() {
|
}
|
||||||
log.info('Falling back to screen API')
|
|
||||||
return screen.info(0)
|
util.inherits(Display, EventEmitter)
|
||||||
})
|
|
||||||
.then(function(display) {
|
Display.prototype.updateRotation = function(newRotation) {
|
||||||
display.url = screen.publicUrl
|
log.info('Rotation changed to %d', newRotation)
|
||||||
return display
|
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
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ module.exports = syrup.serial()
|
||||||
function solve() {
|
function solve() {
|
||||||
log.info('Solving identity')
|
log.info('Solving identity')
|
||||||
var identity = devutil.makeIdentity(options.serial, properties)
|
var identity = devutil.makeIdentity(options.serial, properties)
|
||||||
identity.display = display
|
identity.display = display.properties
|
||||||
identity.phone = phone
|
identity.phone = phone
|
||||||
return identity
|
return identity
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
81
lib/units/device/plugins/screen/capture.js
Normal file
81
lib/units/device/plugins/screen/capture.js
Normal file
|
@ -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
|
||||||
|
})
|
17
lib/units/device/plugins/screen/options.js
Normal file
17
lib/units/device/plugins/screen/options.js
Normal file
|
@ -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
|
||||||
|
})
|
394
lib/units/device/plugins/screen/stream.js
Normal file
394
lib/units/device/plugins/screen/stream.js
Normal file
|
@ -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)
|
||||||
|
})
|
108
lib/units/device/plugins/screen/util/banner.js
Normal file
108
lib/units/device/plugins/screen/util/banner.js
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
40
lib/units/device/plugins/screen/util/broadcastset.js
Normal file
40
lib/units/device/plugins/screen/util/broadcastset.js
Normal file
|
@ -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
|
22
lib/units/device/plugins/screen/util/frameconfig.js
Normal file
22
lib/units/device/plugins/screen/util/frameconfig.js
Normal file
|
@ -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
|
56
lib/units/device/plugins/screen/util/frameparser.js
Normal file
56
lib/units/device/plugins/screen/util/frameparser.js
Normal file
|
@ -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
|
26
lib/units/device/plugins/screen/util/statequeue.js
Normal file
26
lib/units/device/plugins/screen/util/statequeue.js
Normal file
|
@ -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
|
|
@ -1,4 +1,5 @@
|
||||||
var util = require('util')
|
var util = require('util')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
var Promise = require('bluebird')
|
var Promise = require('bluebird')
|
||||||
var syrup = require('stf-syrup')
|
var syrup = require('stf-syrup')
|
||||||
|
@ -87,6 +88,14 @@ module.exports = syrup.serial()
|
||||||
return {
|
return {
|
||||||
bin: resources.bin.dest
|
bin: resources.bin.dest
|
||||||
, lib: resources.lib.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
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
63
res/app/components/stf/screen/rotator-test.js
Normal file
63
res/app/components/stf/screen/rotator-test.js
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
33
res/app/components/stf/screen/rotator.js
Normal file
33
res/app/components/stf/screen/rotator.js
Normal file
|
@ -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]
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
var _ = require('lodash')
|
var _ = require('lodash')
|
||||||
|
var rotator = require('./rotator')
|
||||||
|
|
||||||
module.exports = function DeviceScreenDirective(
|
module.exports = function DeviceScreenDirective(
|
||||||
$document
|
$document
|
||||||
|
@ -47,182 +48,6 @@ module.exports = function DeviceScreenDirective(
|
||||||
* This section should deal with updating the screen ONLY.
|
* This section should deal with updating the screen ONLY.
|
||||||
*/
|
*/
|
||||||
;(function() {
|
;(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() {
|
function stop() {
|
||||||
try {
|
try {
|
||||||
ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null
|
ws.onerror = ws.onclose = ws.onmessage = ws.onopen = null
|
||||||
|
@ -235,6 +60,8 @@ module.exports = function DeviceScreenDirective(
|
||||||
var ws = new WebSocket(device.display.url)
|
var ws = new WebSocket(device.display.url)
|
||||||
ws.binaryType = 'blob'
|
ws.binaryType = 'blob'
|
||||||
|
|
||||||
|
var cleanupList = []
|
||||||
|
|
||||||
ws.onerror = function errorListener() {
|
ws.onerror = function errorListener() {
|
||||||
// @todo Handle
|
// @todo Handle
|
||||||
}
|
}
|
||||||
|
@ -244,80 +71,252 @@ module.exports = function DeviceScreenDirective(
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onopen = function openListener() {
|
ws.onopen = function openListener() {
|
||||||
checkEnabled()
|
var canvas = element.find('canvas')[0]
|
||||||
}
|
, g = canvas.getContext('2d')
|
||||||
|
|
||||||
ws.onmessage = function messageListener(message) {
|
function vendorBackingStorePixelRatio(g) {
|
||||||
updating = false
|
return g.webkitBackingStorePixelRatio ||
|
||||||
|
g.mozBackingStorePixelRatio ||
|
||||||
|
g.msBackingStorePixelRatio ||
|
||||||
|
g.oBackingStorePixelRatio ||
|
||||||
|
g.backingStorePixelRatio || 1
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldUpdateScreen()) {
|
var devicePixelRatio = window.devicePixelRatio || 1
|
||||||
screen.rotation = device.display.rotation
|
, backingStoreRatio = vendorBackingStorePixelRatio(g)
|
||||||
|
, frontBackRatio = devicePixelRatio / backingStoreRatio
|
||||||
|
|
||||||
if (message.data instanceof Blob) {
|
var options = {
|
||||||
if (scope.displayError) {
|
autoScaleForRetina: true
|
||||||
scope.$apply(function () {
|
, density: Math.max(1, Math.min(1.5, devicePixelRatio || 1))
|
||||||
scope.displayError = false
|
, 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], {
|
if (sh < (f = device.display.height * options.minscale)) {
|
||||||
type: 'image/jpeg'
|
sw *= f / sw
|
||||||
})
|
sh *= f / sh
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img.onerror = function() {
|
return {
|
||||||
// Happily ignore. I suppose this shouldn't happen, but
|
w: Math.ceil(sw)
|
||||||
// sometimes it does, presumably when we're loading images
|
, h: Math.ceil(sh)
|
||||||
// too quickly.
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var url = URL.createObjectURL(blob)
|
// FIXME: element is an object HTMLUnknownElement in IE9
|
||||||
img.src = url
|
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 {
|
else {
|
||||||
switch (message.data) {
|
g.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
case 'secure_on':
|
onScreenInterestLost()
|
||||||
scope.$apply(function () {
|
|
||||||
scope.displayError = 'secure'
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next please
|
cachedEnabled = newEnabled
|
||||||
maybeLoadScreen()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: instead of fa-pane-resize, a fa-child-pane-resize could be better
|
function onScreenInterestGained() {
|
||||||
scope.$on('fa-pane-resize', _.throttle(updateBounds, 16))
|
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 () {
|
scope.retryLoadingScreen = function () {
|
||||||
if (scope.displayError === 'secure') {
|
if (scope.displayError === 'secure') {
|
||||||
control.home()
|
control.home()
|
||||||
}
|
}
|
||||||
$timeout(maybeLoadScreen, 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.$watch('device.using', checkEnabled)
|
|
||||||
scope.$on('visibilitychange', checkEnabled)
|
|
||||||
scope.$watch('$parent.showScreen', checkEnabled)
|
|
||||||
|
|
||||||
scope.$on('guest-portrait', function () {
|
scope.$on('guest-portrait', function () {
|
||||||
control.rotate(0)
|
control.rotate(0)
|
||||||
})
|
})
|
||||||
|
|
BIN
vendor/minicap/bin/arm64-v8a/minicap
vendored
BIN
vendor/minicap/bin/arm64-v8a/minicap
vendored
Binary file not shown.
BIN
vendor/minicap/bin/arm64-v8a/minicap-nopie
vendored
BIN
vendor/minicap/bin/arm64-v8a/minicap-nopie
vendored
Binary file not shown.
BIN
vendor/minicap/bin/armeabi-v7a/minicap
vendored
BIN
vendor/minicap/bin/armeabi-v7a/minicap
vendored
Binary file not shown.
BIN
vendor/minicap/bin/armeabi-v7a/minicap-nopie
vendored
BIN
vendor/minicap/bin/armeabi-v7a/minicap-nopie
vendored
Binary file not shown.
BIN
vendor/minicap/bin/x86/minicap
vendored
BIN
vendor/minicap/bin/x86/minicap
vendored
Binary file not shown.
BIN
vendor/minicap/bin/x86/minicap-nopie
vendored
BIN
vendor/minicap/bin/x86/minicap-nopie
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-14/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-14/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-15/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-15/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-16/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-16/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-17/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-17/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-18/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-18/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-19/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-19/x86/minicap.so
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vendor/minicap/shared/android-21/x86/minicap.so
vendored
BIN
vendor/minicap/shared/android-21/x86/minicap.so
vendored
Binary file not shown.
BIN
vendor/minicap/shared/android-21/x86_64/minicap.so
vendored
BIN
vendor/minicap/shared/android-21/x86_64/minicap.so
vendored
Binary file not shown.
BIN
vendor/minicap/shared/android-22/arm64-v8a/minicap.so
vendored
Executable file
BIN
vendor/minicap/shared/android-22/arm64-v8a/minicap.so
vendored
Executable file
Binary file not shown.
BIN
vendor/minicap/shared/android-22/armeabi-v7a/minicap.so
vendored
Executable file
BIN
vendor/minicap/shared/android-22/armeabi-v7a/minicap.so
vendored
Executable file
Binary file not shown.
BIN
vendor/minicap/shared/android-22/x86/minicap.so
vendored
Executable file
BIN
vendor/minicap/shared/android-22/x86/minicap.so
vendored
Executable file
Binary file not shown.
BIN
vendor/minicap/shared/android-22/x86_64/minicap.so
vendored
Executable file
BIN
vendor/minicap/shared/android-22/x86_64/minicap.so
vendored
Executable file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue