STF系列之一--STF 實(shí)時(shí)顯示設(shè)備截圖功能源碼分析

當(dāng)我使用STF時(shí),最震驚的是,它怎么做到設(shè)備和前端頁面設(shè)備模塊操作上的同步掏导。之前看STF 框架之 minicap 工具, 知道作者開發(fā)自己的Android設(shè)備上快速截圖的工具,但是STF怎么將截圖以這么快的速度傳輸?shù)角岸隧撁娴哪乩炜猓亢芎闷媸鸦郏杂辛诉@篇文章。
基礎(chǔ)準(zhǔn)備

STF依賴技術(shù)

  • STF的服務(wù)端基于node js渺蒿,使用express框架
  • STF的前端基于angular 1.x框架

閱讀STF源碼痢士,除熟悉javascript 基礎(chǔ)語法,express框架需要知道一些基本概念茂装。若想要改造STF前端怠蹂,angular 1.x框架必須好好學(xué)一學(xué)。

Websocket協(xié)議

STF服務(wù)端和STF前端通信協(xié)議是Websocket少态,不是HTTP城侧。Websocket是瀏覽器端新的傳輸協(xié)議,類似于socket彼妻。因?yàn)檫@個(gè)協(xié)議嫌佑,STF能快速將截圖從服務(wù)端同步給前端。我們先了解這個(gè)協(xié)議侨歉。

  • STF為什么選擇Websocket協(xié)議

    • 我們假設(shè)瀏覽器是A端屋摇,服務(wù)端是B端,手機(jī)是C端幽邓。STF需要保證C端的操作炮温,能在A端立即反應(yīng)。同時(shí)用戶在A端的點(diǎn)擊之類的事件也能立刻在C端同步牵舵。它不再是像HTTP協(xié)議一樣茅特,單方向通信,而是雙向通信棋枕。正是因?yàn)檫@樣白修,STF使用的是Websocket協(xié)議,
    • Websocket協(xié)議快重斑。除了第一次建立握手鏈接時(shí)兵睛,使用的是http協(xié)議。接下來傳遞數(shù)據(jù)窥浪,使用的是socket協(xié)議祖很。
  • 基于Websocket協(xié)議一段小應(yīng)用
    了解Websocket協(xié)議是理解STF實(shí)時(shí)顯示設(shè)備截圖的基礎(chǔ)。

    • 服務(wù)端 server.js

          var websocketServer = require('socket.io'); //socket.io實(shí)現(xiàn)了websocket協(xié)議漾脂,引用socket.io模塊假颇,新建Websocket服務(wù)器
          var io = new websocketServer(7001);   //Websocket服務(wù)器監(jiān)聽7100端口
          
          io.on('connection', function (ws) {
              ws.emit('news', {hello: "world"}); //連接建立,服務(wù)端發(fā)送消息至前端,消息的標(biāo)識碼是'news',客戶端通過這個(gè)標(biāo)志碼可以接收{(diào)hello: "world"}數(shù)據(jù)
              
              ws.on('fromClient', function (data) {
                  console.log("this is fromClient" + data ) //服務(wù)端接收客戶端標(biāo)志碼‘fromClient’的數(shù)據(jù)骨稿。
          })
      });
      
    • 前端 home.js

         var socket = io.connect('http://localhost:7001'); //服務(wù)端啟websocket服務(wù)在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)哥桥,客戶端發(fā)送該event消息時(shí),服務(wù)端立刻調(diào)用function激涤。socket.on功能類似
    -  ws.emit(event, data)/ws.send(event, data), 服務(wù)端向客戶端發(fā)送data拟糕。socket.emit/socket.send功能類似


一個(gè)完整使用Websocket協(xié)議通信的例子如上所示。接下來分析STF如何實(shí)現(xiàn)實(shí)時(shí)顯示設(shè)備截圖功能倦踢。

實(shí)時(shí)顯示設(shè)備截圖功能源碼分析

STF實(shí)時(shí)顯示設(shè)備截圖流程

image流程.png

將這個(gè)過程分為 從設(shè)備實(shí)時(shí)傳輸圖片二進(jìn)制文件至前端送滞,以及前端渲染圖片兩個(gè)部分。

實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件源碼分析

STF實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件是來自如下文件:

screenshot.png

stream.js做了兩件事:

  • 從設(shè)備 tcp server 中接收圖片二進(jìn)制文件
  • 將圖片二進(jìn)制文件發(fā)送至前端

不關(guān)心STF強(qiáng)大的截圖工具minicap辱挥,只需要明白圖片二進(jìn)制文件如何從設(shè)備傳輸至前端犁嗅。

1.簡單的實(shí)時(shí)傳輸圖片二進(jìn)制文件到前端頁面的demo

STF官方文檔minicap的使用demo,這個(gè)demo實(shí)現(xiàn)了這樣一個(gè)功能:

安裝minicap工具在手機(jī)上般贼,執(zhí)行命令adb forward tcp:1717 localabstract:minicap愧哟,此時(shí)將設(shè)備的TCP服務(wù)器端口映射到本機(jī)的1717端口。nodejs啟動(dòng)代碼中app.js哼蛆,發(fā)現(xiàn)手機(jī)上的截圖不停顯示在localhost:9002頁面上蕊梧。這個(gè)demo是STF中傳輸設(shè)備圖片二進(jìn)制文件到前端的基本雛形。分析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部分

    • 創(chuàng)建Websocket服務(wù)器腮介,用于和前端通信肥矢。

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

          wss.on('connection', function(ws){
              ....
          })
      
    • 關(guān)閉websocket

          ws.on('close', function() {
          ...
      })
      
  • 和設(shè)備建立TCP通信部分

    • 創(chuàng)建tcp client,net模塊是用來創(chuàng)建TCP客戶端叠洗。這段代碼創(chuàng)建一個(gè)TCP客戶端甘改,監(jiān)聽端口1717

      net = require('net')
      var stream = net.connect({
          port: 1717
      })
      
    • 接收tcp server 發(fā)送圖片

      stream.on('readable', tryRead)
      

      當(dāng)接收readable事件后,調(diào)用tryRead函數(shù)灭抑。tryRead除了處理圖片二進(jìn)制文件的邏輯十艾,最重要的是調(diào)用了websocket.send,也就是說從設(shè)備獲得圖片二進(jìn)制文件之后腾节,使用Websocket協(xié)議傳輸至前端忘嫉。

          function tryRead() {
              ....
              //...處理圖片
              
              ws.send(frameBody, {
                        binary: true
                      })
            }
      
    • 關(guān)閉tcp client

       stream.end()
      
  • demo的程序執(zhí)行流程

simpleDemo.png

2.STF中實(shí)時(shí)傳輸設(shè)備截圖代碼分析

STF中stream.js 實(shí)現(xiàn)實(shí)時(shí)傳輸設(shè)備圖片二進(jìn)制文件代碼,基本原理和上面的demo是一樣的案腺。只不過因?yàn)镾TF管理多臺設(shè)備庆冕,代碼會(huì)有點(diǎn)差別。

  • 三個(gè)對象劈榨。

    • FrameProducer

      FrameProducer創(chuàng)建tcp client,解析來自tcp server的數(shù)據(jù)访递,獲得二進(jìn)制文件(圖片)

    • ws

      創(chuàng)建websocket服務(wù)器,和前端通信

    • broadcastSet

      通過broadcastSet的wsFrameNotifier函數(shù)同辣,使用ws拷姿,發(fā)送二進(jìn)制文件(圖片)惭载。

  • 啟動(dòng)實(shí)時(shí)截圖服務(wù)
    [圖片上傳失敗...(image-242b68-1521094554165)]

    • 前端使用websocket傳遞message,當(dāng)message為on時(shí)跌前,調(diào)用broadcastSet.insert()函數(shù)棕兼。
    • FrameProducer.start() 函數(shù)在狀態(tài)隊(duì)列中插入start狀態(tài)陡舅。
    • FrameProducer._ensureState() 開始實(shí)時(shí)同步設(shè)備的圖片二進(jìn)制文件到前端
  • 實(shí)時(shí)同步設(shè)備的圖片二進(jìn)制文件到前端

    實(shí)時(shí)將設(shè)備的圖片二進(jìn)制文件同步到前端抵乓,邏輯放在FrameProducer._ensureState函數(shù)中,代碼如下所示:

     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時(shí)的邏輯靶衍,這段代碼調(diào)用順序如下所示

startScreenshot.png
其中主要函數(shù):
- FrameProducer._connectService: 使用adb命令將設(shè)備的minicap工具啟動(dòng)的tcp server 端口映射到pc的端口A灾炭。創(chuàng)建tcp client,tcp client連接端口A颅眶,返回該tcp client
- FrameProducer._readFrames: 等待minicap發(fā)出`readable`事件蜈出。接收該事件,調(diào)用FrameProducer.emit等函數(shù)涛酗。
- FrameProducer.nextFrame: 解析并返回設(shè)備傳輸二進(jìn)制文件(圖片)铡原,代碼邏輯類似于上面demo中tryRead()函數(shù)。
- Websocket.send: 發(fā)送FrameProducer.nextFrame函數(shù)產(chǎn)生的二進(jìn)制文件(圖片)至前端

前端渲染圖片

前端接收到二進(jìn)制文件商叹,如何渲染圖片呢燕刻?這部分邏輯主要在${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: 接收來自服務(wù)端的圖片二進(jìn)制文件,為它創(chuàng)造blob對象
  • URL.createObjectURL: 為blob對象創(chuàng)建URL,可以像普通URL使用它
  • 將URL賦值給img.src剖笙,圖片可以加載出來

單獨(dú)拎出來這段代碼卵洗。這種更新前端圖片的流程給我提供了新思路。

后記

STF實(shí)時(shí)顯示設(shè)備截圖功能涉及的知識點(diǎn)很多:Android弥咪,tcp通信过蹂,瀏覽器Websocket協(xié)議,blob對象等聚至。只覺得寫這個(gè)工具的作者牛X酷勺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扳躬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坦报,更是在濱河造成了極大的恐慌,老刑警劉巖片择,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潜的,死亡現(xiàn)場離奇詭異字管,居然都是意外死亡啰挪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門亡呵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抽活,“玉大人,你說我怎么就攤上這事锰什。” “怎么了汁胆?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵嫩码,是天一觀的道長。 經(jīng)常有香客問我铸题,道長,這世上最難降的妖魔是什么丢间? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任千劈,我火速辦了婚禮,結(jié)果婚禮上墙牌,老公的妹妹穿的比我還像新娘。我一直安慰自己捉捅,他們只是感情好虽风,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著无牵,像睡著了一般厂抖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天谭溉,我揣著相機(jī)與錄音橡卤,去河邊找鬼。 笑死碧库,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的旅挤。 我是一名探鬼主播伞鲫,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼签舞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吠架?” 一聲冷哼從身側(cè)響起搂鲫,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拐辽,沒想到半個(gè)月后擦酌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡睁搭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年笼平,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寓调。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捶牢,死狀恐怖巍耗,靈堂內(nèi)的尸體忽然破棺而出渐排,到底是詐尸還是另有隱情,我是刑警寧澤驯耻,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布可缚,位于F島的核電站霎迫,受9級特大地震影響帘靡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涩赢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一轩勘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧花墩,春花似錦澄步、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搪柑。三九已至索烹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間百姓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工旬迹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奔垦。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓椿猎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親犯眠。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容