Android 投屏到電腦上,類似vysor。不需要網連數據線,延時小。原理是電腦上佈一個socket.io服務器;手機通過socket.io連接到自己佈置的局域網服務器,然後通過ImageRedaer不斷截屏,將畫片發送到局域網服務器。最後一步就是電腦的顯示,服務器上獲得手機傳過來的Bitmap,讓其發送到html網頁,並刷新網頁。於是我們就可以在localhost:3000首頁上看到手機屏幕的內容。這裏涉及到兩個服務器io-server.js和index.js,分別監聽9000端口和3000端口,9000端口的負責監聽並接收手機發送過來的數據,3000端口負責接收電腦瀏覽器發送過來的請求,並給瀏覽器發送圖片。
Android PC投屏簡單嘗試
博客地址 https://www.jianshu.com/p/ce37330365f2
項目使用說明
必須有nodejs的環境 在./sockt目錄下安裝
npm install --save [email protected]
npm install --save socket.io
- 運行Node的Socket服務端
node ./sockt/io-server.js
- 運行Node的網頁
node ./sockt/index.js
打開 localhost:3000.就可以看到網頁了。
- 運行App。進入 在MainActivity,點擊
Start
按鈕,就可以開始了 進入Activity時,已經回去連接socket
注意:
- 需要在局域網內運行。App內需要配置好Socket鏈接的ip. 在
SocketIoManager
內 - 具體的內容,還是請看一下代碼。
###效果預覽
###簡單說明:
- 使用Android MediaProjection Api來完成視頻的截圖
- 通過WebSocket進行鏈接。將圖片傳遞給網頁
想法來源
看到vysor
,覺得特別好玩,於是就想着自己能不能試着做一個類似的功能出來。搜索了相關實現。發現網上已經有網友針對vysor
做了分析。於是就照着思路,按圖索驥,當作對MediaProjection Api的練習,來完成這個小項目
主要思路
####1. 獲取屏幕的截屏
- 創建VirtualDisplay Android在Api 21以上爲我們已經提供了系統的Api可以進行操作。 主要是這幾個類的相互配合
MediaProjection
和VirtualSurface
,還有截圖的話,使用ImageReader
,三個類配合使用。
public RxScreenShot createImageReader() { //注意這裏使用RGB565報錯提示,只能使用RGBA_8888 mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1000); mSurfaceFactory = new ImageReaderSurface(mImageReader); createProject(); return this; } private void createProject() { mediaProjection.registerCallback(mMediaCallBack, mCallBackHandler); //通過這種方式來創建這個VirtualDisplay,並將數據傳遞給ImageReader提供surface mediaProjection.createVirtualDisplay(TAG + "-display", width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurfaceFactory.getInputSurface(), null, null); }
- 獲取屏幕截圖 可以通過
ImageReader
類。配套Image
來獲獎獲得的數據轉成Bitmap
/* 封裝成了Observable對象。 */ public class ImageReaderAvailableObservable extends Observable<ImageReader> { public static ImageReaderAvailableObservable of(ImageReader imageReader) { return new ImageReaderAvailableObservable(imageReader, null); } public static ImageReaderAvailableObservable of(ImageReader imageReader,Handler handler) { return new ImageReaderAvailableObservable(imageReader, handler); } private final ImageReader imageReader; private final Handler handler; private ImageReaderAvailableObservable(ImageReader imageReader, Handler handler) { this.imageReader = imageReader; this.handler = handler; } @Override protected void subscribeActual(Observer<? super ImageReader> observer) { Listener listener = new Listener(observer, imageReader); observer.onSubscribe(listener); //設置準備好的監聽事件 imageReader.setOnImageAvailableListener(listener, handler); } static class Listener implements Disposable, ImageReader.OnImageAvailableListener { private final AtomicBoolean unsubscribed = new AtomicBoolean(); private final ImageReader mImageReader; private final Observer<? super ImageReader> observer; Listener(Observer<? super ImageReader> observer, ImageReader imageReader) { this.mImageReader = imageReader; this.observer = observer; } @Override public void onImageAvailable(ImageReader reader) { if (!isDisposed()) { //將準備好的reader發送出去,進行處理 observer.onNext(reader); //注意:這裏如果不調用onCompleted事件。其實這個監聽會不斷回調事件 // observer.onComplete(); } } @Override public void dispose() { if (unsubscribed.compareAndSet(false, true)) { mImageReader.setOnImageAvailableListener(null, null); } } @Override public boolean isDisposed() { return unsubscribed.get(); } } } /* 調用開始截屏的方法 */ public Observable<Object> startCapture() { return ImageReaderAvailableObservable.of(mImageReader) .map(imageReader -> { String mImageName = System.currentTimeMillis() + ".png"; Log.e(TAG, "image name is : " + mImageName); Bitmap bitmap = null; //從imageReader中獲取到最新的Image Image image = imageReader.acquireLatestImage(); if (image == null) { } else { //將Image對象轉成bitmap int width = image.getWidth(); int height = image.getHeight(); //byteBuffer都保存在image.Plane中 final Image.Plane[] planes = image.getPlanes(); final ByteBuffer buffer = planes[0].getBuffer(); int pixelStride = planes[0].getPixelStride(); int rowStride = planes[0].getRowStride(); int rowPadding = rowStride - pixelStride * width; bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(buffer); bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height); //這裏使用完要記得close.如果沒有close,當imageReader達到max_count上限時將會拋出異常 image.close(); } return bitmap == null ? new Object() : bitmap; }); }
這裏需要注意的是,需要通過這個回調,每當屏幕發生變化,就會回調這個接口,可以得到最新的截圖。ImageReader::setOnImageAvailableListener
2. 搭建Socket連接,將圖片的數據進行傳遞
node 部分的代碼在 https://github.com/deepsadness/MediaProjectionDemo/tree/master/sockt
因爲我們的目標是在網頁內打開,所以需要和網頁進行通信。 可以簡單的使用WebSocket
進行雙方通向
通過Socket.io
https://socket.io/ 就可以簡單的實現
- Android端的代碼 通過WebSocket將Bitmap的字節碼發送出去
private fun sendBitmap(it: Bitmap) { val byteArrayOutputStream = ByteArrayOutputStream() it.compress(Bitmap.CompressFormat.JPEG, 60, byteArrayOutputStream) val byteArray = byteArrayOutputStream.toByteArray() SocketIoManager.getInstance().send(byteArray) } public void send(byte[] bitmapArray) { if (!mSocketReady) { return; } if (bitmapArray != null) { mSocket.emit("event", bitmapArray); } }
- Node端的代碼 簡單的SocketIo實現.代碼在
/sockt/io-server.js
var io = require('socket.io')(); var clients = [] io.on('connection', function (client) { clients.push(client); console.log('connection!'); client.emit('join', 'welcome to join!!') client.on('chat message', function (msg) { console.log("receive msg=" + msg); }); client.on('event', function (msg) { // console.log("event", msg); console.log("event", "send image~~"); //通過event事件出去 clients.forEach(function (it) { it.emit('event', msg) }) }); }); io.on('disconnect', function (client) { }) io.listen(9000);
3. 如何將圖片顯示出來
代碼在 /sockt/index.html
中 html
中的src
就可以直接對傳遞byte[]
的進行解析。
socket.on('image', function (msg) { var arrayBufferView = new Uint8Array(msg); var blob = new Blob([arrayBufferView], { type: "image/jpeg" }); var urlCreator = window.URL || window.webkitURL; var imageUrl = urlCreator.createObjectURL(blob); var img = document.getElementById("screen"); // var img = document.querySelector("#photo"); img.src = imageUrl;
由於https://github.com/deepsadness/MediaProjectionDemo這個例子裏有部分代碼是用kotlin寫的,
我將其改成了java代碼。代碼地址如下:https://github.com/tomyZhou/AndroidScreentShot