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