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

shuta · 2017年05月27日 · 最后由 jie 回复于 2018年04月25日 · 134 次阅读

当我使用 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 STF 系列之二---minitouch 流程源码分析 中提及了此贴 06月04日 09:24
4楼 已删除

学习了

mrx102 STF 集成 iOS 之远程控制 中提及了此贴 05月17日 10:55
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册