STF STF 实时显示设备截图功能源码分析

shuta · May 27, 2017 · Last by jie replied at April 25, 2018 · 6245 hits

当我使用STF时,最震惊的是,它怎么做到设备和前端页面设备模块操作上的同步。之前看STF 框架之 minicap 工具, 知道作者开发自己的Android设备上快速截图的工具,但是STF怎么将截图以这么快的速度传输到前端页面的呢?很好奇,所以有了这篇文章。

基础准备

STF依赖技术

  • STF的服务端基于node js,使用express框架
  • STF的前端基于angular 1.x框架

阅读STF源码,除熟悉javascript 基础语法,express框架需要知道一些基本概念。若想要改造STF前端,angular 1.x框架必须好好学一学。

Websocket协议

STF服务端和STF前端通信协议是Websocket,不是HTTP。Websocket是浏览器端新的传输协议,类似于socket。因为这个协议,STF能快速将截图从服务端同步给前端。我们先了解这个协议。

  • STF为什么选择Websocket协议

    • 我们假设浏览器是A端,服务端是B端,手机是C端。STF需要保证C端的操作,能在A端立即反应。同时用户在A端的点击之类的事件也能立刻在C端同步。它不再是像HTTP协议一样,单方向通信,而是双向通信。正是因为这样,STF使用的是Websocket协议,
    • Websocket协议快。除了第一次建立握手链接时,使用的是http协议。接下来传递数据,使用的是socket协议。
  • 基于Websocket协议一段小应用
    了解Websocket协议是理解STF实时显示设备截图的基础。

    • 服务端 server.js

          var websocketServer = require('socket.io'); //socket.io实现了websocket协议,引用socket.io模块,新建Websocket服务器
      var io = new websocketServer(7001); //Websocket服务器监听7100端口

      io.on('connection', function (ws) {
      ws.emit('news', {hello: "world"}); //连接建立,服务端发送消息至前端,消息的标识码是'news',客户端通过这个标志码可以接收{hello: "world"}数据

      ws.on('fromClient', function (data) {
      console.log("this is fromClient" + data ) //服务端接收客户端标志码‘fromClient’的数据。
      })
      });
    • 前端 home.js

      var socket = io.connect('http://localhost:7001'); //服务端启websocket服务在7100端口,所以客户端连接7100端口
      socket.on('news', function (data) {
      console.log(data.hello)
      });

      socket.emit('fromClient',{"message": "everything is ok"});
    • 上述demo中,使用的socket.io模块,这也是STF使用模块,说明如下:

      • io.on('connection',function) , 与客户端Websocket连接建立成功
      • ws.on(event, function),客户端发送该event消息时,服务端立刻调用function。socket.on功能类似
      • ws.emit(event, data)/ws.send(event, data), 服务端向客户端发送data。socket.emit/socket.send功能类似

    一个完整使用Websocket协议通信的例子如上所示。接下来分析STF如何实现实时显示设备截图功能。

实时显示设备截图功能源码分析

STF实时显示设备截图流程

将这个过程分为 从设备实时传输图片二进制文件至前端,以及前端渲染图片两个部分。

实时传输设备图片二进制文件源码分析

STF实时传输设备图片二进制文件是来自如下文件:

stream.js做了两件事:

  • 从设备 tcp server 中接收图片二进制文件
  • 将图片二进制文件发送至前端

不关心STF强大的截图工具minicap,只需要明白图片二进制文件如何从设备传输至前端。

1.简单的实时传输图片二进制文件到前端页面的demo

STF官方文档minicap的使用demo,这个demo实现了这样一个功能:

安装minicap工具在手机上,执行命令adb forward tcp:1717 localabstract:minicap,此时将设备的TCP服务器端口映射到本机的1717端口。nodejs启动代码中app.js,发现手机上的截图不停显示在localhost:9002页面上。这个demo是STF中传输设备图片二进制文件到前端的基本雏形。分析demo中app.js

var WebSocketServer = require('ws').Server
, http = require('http')
, express = require('express')
, path = require('path')
, net = require('net')
, app = express()

var PORT = process.env.PORT || 9002

app.use(express.static(path.join(__dirname, '/public')))

var server = http.createServer(app)
var wss = new WebSocketServer({ server: server })

wss.on('connection', function(ws) {
console.info('Got a client')

var stream = net.connect({
port: 1717
})

stream.on('error', function() {
console.error('Be sure to run `adb forward tcp:1717 localabstract:minicap`')
process.exit(1)
})


function tryRead() {
....
....

ws.send(frameBody, {
binary: true
})
}

stream.on('readable', tryRead)

ws.on('close', function() {
console.info('Lost a client')
stream.end()
})

server.listen(PORT)

上述代码主要分为以下几块

  • 和前端通信的websocket部分

    • 创建Websocket服务器,用于和前端通信。

      var WebSocketServer = require('ws').Server
      var server = http.createServer(app)
      var wss = new WebSocketServer({ server: server })
    • websocket连接建立成功。

      wss.on('connection', function(ws){
      ....
      })
    • 关闭websocket

          ws.on('close', function() {
      ...
      })
  • 和设备建立TCP通信部分

    • 创建tcp client,net模块是用来创建TCP客户端。这段代码创建一个TCP客户端,监听端口1717

      net = require('net')
      var stream = net.connect({
      port: 1717
      })
    • 接收tcp server 发送图片

      stream.on('readable', tryRead)

      当接收readable事件后,调用tryRead函数。tryRead除了处理图片二进制文件的逻辑,最重要的是调用了websocket.send,也就是说从设备获得图片二进制文件之后,使用Websocket协议传输至前端。

      function tryRead() {
      ....
      //...处理图片

      ws.send(frameBody, {
      binary: true
      })
      }
    • 关闭tcp client

      stream.end()
  • demo的程序执行流程

2.STF中实时传输设备截图代码分析

STF中stream.js 实现实时传输设备图片二进制文件代码,基本原理和上面的demo是一样的。只不过因为STF管理多台设备,代码会有点差别。

  • 三个对象。

    • FrameProducer

      FrameProducer创建tcp client,解析来自tcp server的数据,获得二进制文件(图片)

    • ws

      创建websocket服务器,和前端通信

    • broadcastSet

      通过broadcastSet的wsFrameNotifier函数,使用ws,发送二进制文件(图片)。

  • 启动实时截图服务

    • 前端使用websocket传递message,当message为on时,调用broadcastSet.insert()函数。
    • FrameProducer.start() 函数在状态队列中插入start状态。
    • FrameProducer._ensureState() 开始实时同步设备的图片二进制文件到前端
  • 实时同步设备的图片二进制文件到前端

    实时将设备的图片二进制文件同步到前端,逻辑放在FrameProducer._ensureState函数中,代码如下所示:

     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 = new RiskyStream(out)
    .on('unexpectedEnd', this._outputEnded.bind(this))
    return this._readOutput(this.output.stream)
    })
    .then(function() {
    return this._waitForPid()
    })
    .then(function() {
    return this._connectService()
    })
    .then(function(socket) {
    this.parser = new FrameParser()
    this.socket = new RiskyStream(socket)
    .on('unexpectedEnd', this._socketEnded.bind(this))
    return this._readBanner(this.socket.stream)
    })
    .then(function(banner) {
    this.banner = banner
    return this._readFrames(this.socket.stream)
    })
    .then(function() {
    this.runningState = FrameProducer.STATE_STARTED
    this.emit('start')
    })
    .catch(Promise.CancellationError, function() {
    return this._stop()
    })
    .catch(function(err) {
    return this._stop().finally(function() {
    this.failCounter.inc()
    this.emit('error', err)
    })
    })
    .finally(function() {
    this._ensureState()
    })
    }
    else {
    setImmediate(this._ensureState.bind(this))
    }
    break
    ....
    ....
    }

    上面这段代码主要看FrameProducer.STATE_STOPPED时的逻辑,这段代码调用顺序如下所示

    其中主要函数:

    • FrameProducer._connectService: 使用adb命令将设备的minicap工具启动的tcp server 端口映射到pc的端口A。创建tcp client,tcp client连接端口A,返回该tcp client
    • FrameProducer._readFrames: 等待minicap发出readable事件。接收该事件,调用FrameProducer.emit等函数。
    • FrameProducer.nextFrame: 解析并返回设备传输二进制文件(图片),代码逻辑类似于上面demo中tryRead()函数。
    • Websocket.send: 发送FrameProducer.nextFrame函数产生的二进制文件(图片)至前端

前端渲染图片

前端接收到二进制文件,如何渲染图片呢?这部分逻辑主要在${STFhome}/res/app/components/stf/screen/screen-directive.js文件中

var ws = new WebSocket(device.display.url)
ws.binaryType = 'blob'

ws.onmessage = (function() {

return function messageListener(message) {
if (message.data instanceof Blob) {
var blob = new Blob([message.data], {
type: 'image/jpeg'
})

...
...

var img = imagePool.next()

var url = URL.createObjectURL(blob)

img.src = url
}
}
})()
  • new Blob: 接收来自服务端的图片二进制文件,为它创造blob对象
  • URL.createObjectURL: 为blob对象创建URL,可以像普通URL使用它
  • 将URL赋值给img.src,图片可以加载出来

单独拎出来这段代码。这种更新前端图片的流程给我提供了新思路。

后记

STF实时显示设备截图功能涉及的知识点很多:Android,tcp通信,浏览器Websocket协议,blob对象等。只觉得写这个工具的作者牛X。

共收到 3 条回复 时间 点赞

把截图换成传输图片二进制数据更为准确,不然不会这么快。

shuta #2 · May 27, 2017 作者

对,传输的是图片二进制数据。我修改一下文章。

shuta STF 系列之二---minitouch 流程源码分析 中提及了此贴 04 Jun 17:24
4Floor has been deleted

学习了

mrx102 STF 集成 iOS 之远程控制 中提及了此贴 17 May 18:55
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up