【译】Introducing scrcpy
我開發(fā)了一個應用程序來顯示和控制連接在USB上的Android設備。?它不需要任何root訪問權限。?它適用于GNU / Linux,Windows和Mac OS。
它側重于:
- 亮度?(原生,僅顯示設備屏幕)
- 表演?(30~60fps)
- 質量?(1920×1080或以上)
- 低延遲?(70~100ms)
- 啟動時間短?(顯示第一張圖像約1秒)
- 非侵入性?(設備上沒有安裝任何東西)
就像我之前的項目gnirehtet一樣?,?Genymobile接受了開源:?scrcpy?。
您可以構建,安裝和運行它。
scrcpy如何工作?
應用程序在設備上執(zhí)行服務器。?客戶端和服務器通過adb隧道上的套接字進行通信。
服務器流式傳輸設備屏幕的H.264視頻。?客戶端解碼視頻幀并顯示它們。
客戶端捕獲輸入(鍵盤和鼠標)事件,將它們發(fā)送到服務器,服務器將它們注入設備。
文檔提供了更多詳細信息。
在這里,我將詳細介紹應用程序可能感興趣的應用程序的幾個技術方面。
最大限度地減少延遲
沒有緩沖
編碼,傳輸和解碼視頻流需要時間。?為了減少延遲,我們必須避免任何額外的延遲。
例如,讓我們使用screenrecord流式傳輸屏幕并使用VLC播放:
adb exec-out screenrecord --output-format=h264 - | vlc - --demux h264最初,它可以工作,但很快就會延遲并且?guī)黄茐摹?原因是VLC將PTS與幀相關聯(lián),并緩沖流以在某個目標時間播放幀。
因此,它有時會在stderr上打印出這樣的錯誤:
ES_OUT_SET_(GROUP_)PCR is called too late (pts_delay increased to 300 ms)就在我開始這個項目之前,與WebRTC一起工作的同事Philippe建議我“手動”解碼(使用FFmpeg?)并渲染幀,以避免任何額外的延遲。?這使我免于浪費時間,這是正確的解決方案。
解碼視頻流以使用FFmpeg檢索單個幀非常簡單?。
跳過幀
如果由于任何原因,渲染被延遲,則丟棄解碼的幀,以便scrcpy始終顯示最后一個解碼的幀。
請注意,可以使用配置標志更改此行為:
mesonconf x -Dskip_frames=false在Android上運行Java main
捕獲設備屏幕需要一些權限,這些權限授予shell?。
通過從adb shell調(diào)用app_process?,可以在Android上執(zhí)行Java代碼作為adb shell?。
你好,世界!
這是一個簡單的Java應用程序:
public class HelloWorld { public static void main ( String ... args ) { System . out . println ( "Hello, world!" ); } }讓我們編譯并解釋它:
javac -source 1.7 -target 1.7 HelloWorld.java "$ANDROID_HOME"/build-tools/27.0.2/dx \ --dex --output classes.dex HelloWorld.class然后,我們將classes.dex推送到Android設備:
adb push classes.dex /data/local/tmp/并執(zhí)行它:
$ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld Hello, world!訪問Android框架
應用程序可以在運行時訪問Android框架。
例如,讓我們使用android.os.SystemClock?:
import android.os.SystemClock ; public class HelloWorld { public static void main ( String ... args ) { System . out . print ( "Hello," ); SystemClock . sleep ( 1000 ); System . out . println ( " world!" ); } }我們將我們的類與android.jar?:
javac -source 1.7 -target 1.7 \ -cp "$ANDROID_HOME"/platforms/android-27/android.jar HelloWorld.java然后像以前一樣運行它。
請注意,scrcpy還需要從框架中訪問隱藏的方法?。?在這種情況下,鏈接android.jar是不夠的,所以它使用反射?。
就像一個APK
如果classes.dex嵌入在zip / jar中,則執(zhí)行也有效:
jar cvf hello.jar classes.dex adb push hello.jar /data/local/tmp/ adb shell CLASSPATH=/data/local/tmp/hello.jar app_process / HelloWorld你知道一個包含classes.dex的zip的例子嗎??一個APK?!
因此,它適用于任何已安裝的APK,其中包含一個帶有main方法的類:
$ adb install myapp.apk … $ adb shell pm path my.app.package package:/data/app/my.app.package-1/base.apk $ adb shell CLASSPATH=/data/app/my.app.package-1/base.apk \ app_process / HelloWorld在scrcpy中
為了簡化構建系統(tǒng),我決定使用gradle將服務器構建為APK,即使它不是真正的Android應用程序:?gradle提供運行測試,檢查樣式??等的任務。
以這種方式調(diào)用,服務器被授權捕獲設備屏幕。
改善啟動時間
快速安裝
用戶無需在設備上安裝任何內(nèi)容:在啟動時,客戶端負責在設備上執(zhí)行服務器。
我們看到我們可以從APK執(zhí)行服務器的主要方法:
- 安裝,或
- 推送到/data/local/tmp?。
哪一個選擇?
$ time adb install server.apk … real 0m0,963s … $ time adb push server.apk /data/local/tmp/ … real 0m0,022s …所以我決定推。
請注意,?/data/local/tmp是shell可讀寫的,但不是/data/local/tmp可寫的,因此惡意應用程序可能無法在客戶端執(zhí)行之前替換服務器。
并行
如果你執(zhí)行了Hello,那么世界!?在上一節(jié)中,您可能已經(jīng)注意到運行app_process需要一些時間:?Hello, World!?在一些延遲之前(0.5到1秒之間)不打印。
在客戶端中,初始化SDL也需要一些時間。
因此,這些初始化步驟已經(jīng)并行化?。
清理設備
使用后,我們要從設備中刪除服務器(?/data/local/tmp/scrcpy-server.jar?)。
我們可以在退出時將其刪除,但之后,它將在設備斷開連接時保留。
相反,一旦app_process打開服務器,?scrcpy?unlink?s(?rm?)就可以了。?因此,文件僅存在不到1秒(甚至在顯示屏幕之前它也被刪除)。
當最后一個關聯(lián)的打開文件描述符關閉時(最遲,當app_process死亡時),實際上刪除了文件本身(不是它的名字)。
處理文本輸入
處理從鍵盤接收的輸入比我想象的更復雜。
活動
有兩種“鍵盤”事件:
- 關鍵事件,
- 文字輸入事件。
鍵事件提供?掃描碼?(鍵盤上鍵的物理位置)和鍵碼?(取決于鍵盤布局)。?scrcpy只使用密鑰?代碼?(它不需要物理密鑰的位置)。
但是,關鍵事件不足以處理文本輸入?:
有時可能需要多次按鍵才能產(chǎn)生角色。?有時一次按鍵可以產(chǎn)生多個字符。
即使是簡單的字符也可能無法通過鍵事件輕松處理,因為它們?nèi)Q于布局。?例如,在法語鍵盤上輸入.?(點)生成Shift?+?;?。
因此,?scrcpy僅針對一組有限的密鑰將密鑰事件轉發(fā)給設備。?其余的由文本輸入事件處理。
注入文字
在Android方面,我們可能不直接注入文本(注入由相關構造函數(shù)創(chuàng)建的KeyEvent不起作用)。?相反,我們可以使用getEvents(char[])檢索為char[]生成的KeyEvent列表。
例如:
char [] chars = { '?' }; KeyEvent [] events = charMap . getEvents ( chars );在這里,使用4個事件的數(shù)組初始化事件:
正確地注入這些事件會產(chǎn)生char?'?'?。
處理重音字符
不幸的是,以前的方法僅適用于ASCII字符:
char [] chars = { 'é' }; KeyEvent [] events = charMap . getEvents ( chars ); // events is null!!!我首先想到?jīng)]有辦法從那里注入這樣的事件,直到我與Philippe討論(是的,和之前一樣),誰知道解決方案:當我們使用組合變音死鍵字符分解字符時它起作用。
具體而言,我們注入"\u0301e"而不是注入"é"?"\u0301e"?:
char [] chars = { '\u0301' , 'e' }; KeyEvent [] events = charMap . getEvents ( chars ); // now, there are events因此,為了支持重音字符,?scrcpy嘗試使用KeyComposition?分解字符。
編輯:重音字符不適用于虛擬鍵盤Gboard(默認的谷歌鍵盤),但使用默認(AOSP)鍵盤和SwiftKey。
設置一個窗口圖標
應用程序窗口可能有一個圖標,用于標題欄(對于某些桌面環(huán)境)和/或桌面任務欄中。
必須通過SDL_SetWindowIcon從SDL_Surface設置窗口圖標。?使用圖標內(nèi)容創(chuàng)建表面取決于開發(fā)人員。?例如,我們可以決定從PNG文件加載圖標,或者直接從內(nèi)存中的原始像素加載圖標。
相反,另一位同事Aurélien建議我使用XPM圖像格式,這也是一個有效的C源代碼:?icon.xpm?。
請注意,圖像不是icon_xpm聲明的變量icon_xpm的內(nèi)容:它是整個文件!?因此,?icon.xpm既可以在Gimp中直接打開,也可以包含在C源代碼中:
#include "icon.xpm"作為一個好處,我們直接“識別”源代碼中的圖標,我們可以輕松地對其進行修補:在調(diào)試模式下,?圖標顏色會發(fā)生變化。
結論
開發(fā)這個項目是一個令人敬畏和激勵的經(jīng)驗。?我學到了很多東西(之前從未使用過SDL或者libav / FFmpeg?)。
由此產(chǎn)生的應用程序比我最初預期的更好,我很高興能夠開源它。
討論reddit和黑客新聞?。
https://blog.rom1v.com/2018/03/introducing-scrcpy/
總結
以上是生活随笔為你收集整理的【译】Introducing scrcpy的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vysor原理代码实现(V2.0)
- 下一篇: Android adb无线调试脚本