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')
|
||||
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'))
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 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
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
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 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)
|
||||
})
|
||||
|
|
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