日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android PC投屏简单尝试—最终章1

發布時間:2025/3/15 Android 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android PC投屏简单尝试—最终章1 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

回顧之前的幾遍文章,我們分別通過RMTP協議和簡單的Socket 發送Bitmap圖片的Base64編碼來完成投屏。
回想這系列文章的想法來源-Vysor,它通過 USB來進行連接的。又看到了 scrcpy項目。
于是有了這個系列的最終章-仿scrcpy(Vysor)
ps:其實就是對著scrcpy的源碼擼了一遍。

效果預覽

簡單的錄制效果.gif

源碼地址:https://github.com/deepsadness/AppRemote

內容目錄

包括的內容有

  • 通過USB連接和adb進行手機通信
  • 在Android端發送錄制屏幕的H264 Naul
  • 使用SDL2和FFmpeg,編寫能夠在PC(Windows,Mac)上運行的投屏界面
  • 1. USB Socket連接

    • 熟悉adb foward 和 adb reverse 命令
      由于Android版本低于5.0版本不支持adb reverse

    adb forward.png

    forward --list list all forward socket connectionsforward [--no-rebind] LOCAL REMOTEforward socket connection using:tcp:<port> (<local> may be "tcp:0" to pick any open port)localabstract:<unix domain socket name>localreserved:<unix domain socket name>localfilesystem:<unix domain socket name>dev:<character device name>jdwp:<process pid> (remote only)forward --remove LOCAL remove specific forward socket connectionforward --remove-all remove all forward socket connections

    這個命令的意思是 將PC上的端口(LOCAL) 轉發到 Android手機上(REMOTE) 。
    這樣的話,我們就可以在Android段建立Server,監聽我們的REMOTE,而PC端可以通過連接連接這個LOCAL,就可以成功的建立Socket連接。
    還可以注意到,我們使用的LOCAL和REMOTE除了可以使用 TCP的端口的形式,還可以使用 UNIX Domain Socket IPC協議 。

    通常我們可以使用
    adb forward tcp:8888 tcp:8888來監聽兩端的端口。
    下面是簡單的調試代碼

    調試代碼

    下面通過兩種方式來進行通信

    使用 tcp port的方式

    • 命令行
      adb forward tcp:8888 tcp:8888
    • socket Server(android 端)
    public class PortServer {public static void start() {new Thread(new Runnable() {@Overridepublic void run() {//可以直接使用抽象的名字作為socket的名稱ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(8888);//blockingSocket accept = serverSocket.accept();Log.d("ZZX", "serverSocket 鏈接成功");while (true) {if (!accept.isConnected()) {return;}InputStream inputStream = accept.getInputStream();String result = IO.streamToString(inputStream);Log.d("ZZX", "serverSocket recv =" + result);}} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}}).start();} }
    • socket client 端
      java 版本
    private static void socket() {//開啟socket clinetSocket clinet = new Socket();//用adb轉發端口到8888try {//blockingclinet.connect(new InetSocketAddress("127.0.0.1", 8888));System.out.println("連接成功!!");OutputStream outputStream = clinet.getOutputStream();outputStream.write("Hello World".getBytes());outputStream.flush();outputStream.close();} catch (IOException e) {e.printStackTrace();}}

    使用 localabstract 的方式

    • 命令行
      adb forward tcp:8888 localabstract: local

    需要修改的只有Server 端。因為我們將服務端(Android端)改成了localabstract的方式。

    • android
    public class LocalServer {public static void start() {new Thread(new Runnable() {@Overridepublic void run() {//可以直接使用抽象的名字作為socket的名稱LocalServerSocket serverSocket = null;try {serverSocket = new LocalServerSocket("local");//blockingLocalSocket client = serverSocket.accept();Log.d("ZZX", "serverSocket 鏈接成功");while (true) {if (!client.isConnected()) {return;}InputStream inputStream = client.getInputStream();String result = IO.streamToString(inputStream);Log.d("ZZX", "serverSocket recv =" + result);}} catch (IOException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}}).start();} }

    這樣我們就通過ADB協議和USB 建立了手機和PC端的通信。之后我們就可以通過這里建立的socket,進行數據傳遞。

    2. 在Android上運行后臺程序

    我們期望運行一個后臺的程序,PC端開啟之后,就開始給我們發送截屏的數據。
    這里涉及了幾個問題。
    第一個是在之前的文章中,我們知道,我們需要進行屏幕的截屏,需要申請對應的權限。
    第二個是,如何直接在Android上運行我們寫好的java程序,還不是用Activity的方式來運行。

    使用app_process 運行程序

    這個命令完美的滿足了我們的需求。
    它不但可以在后臺直接運行dex中的java文件,還具有較高的權限!!!

    調試代碼

    我們先來寫一個簡單的類試一下

    • 編寫一個簡單的java文件
    public class HelloWorld {public static void main(String... args) {System.out.println("Hello, world!");} }
    • 將其編譯成dex文件
    javac -source 1.7 -target 1.7 HelloWorld.java "$ANDROID_HOME"/build-tools/27.0.2/dx \--dex --output classes.dex HelloWorld.class
    • 推送到設備上并執行它
    adb push classes.dex /data/local/tmp/ $ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld

    這樣我們就運行成功了~~

    這里需要注意的是app_process 只能執行dex文件

    • adb_forward
    task adb_forward() {doLast {println "adb_forward"def cmd = "adb forward --list"ProcessBuilder builder = new ProcessBuilder(cmd.split(" "))Process adbpb = builder.start()if (adbpb.waitFor() == 0) {def result = adbpb.inputStream.textprintln result.length()if (result.length() <= 1) {def forward_recorder = "adb forward tcp:9000 localabstract:recorder"ProcessBuilder forward_pb = new ProcessBuilder(forward_recorder.split(" "))Process forward_ps = forward_pb.start()if (forward_ps.waitFor() == 0) {println "forward success!!"}} else {println result}} else {println "error = " + adbpb.errorStream.text}} }

    在AndroidStudio中編譯

    上面我體驗了簡單的通過命令行的形式來編譯。實際上,我們的開發都是在AndroidStudio中的。而且它為我們提供了一個很好的編譯環境。不需要在手動去敲入這些代碼。
    我們只需要通過自定義Gradle Task就可以簡單的完成這個任務。

    • 單純復制dex文件
      因為運行的是dex文件,所以我們直接復制dex文件就行了。
      這個任務必須依賴于assembleDebug任務。因為只有這個任務執行完,才會有這些dex文件。
    //將dex文件復制。通過dependsOn來制定依賴關系 //因為是設定了type是gradle中已經實現了Copy,所以直接配置它的屬性 task class_cls(type: Copy, dependsOn: "assembleDebug") {from "build/intermediates/transforms/dexMerger/debug/0/"destinationDir = file('build/libs') }
    • 壓縮成jar
      如果不直接使用dex的話,也可以壓縮成jar的形式。其實和上面直接使用dex的方式沒差。就看你自己喜歡了。
    //將編譯好的dex文件壓縮成jar. task classex_jar(type: Jar, dependsOn: "assembleDebug") {from "build/intermediates/transforms/dexMerger/debug/0/"destinationDir = file('build/libs')archiveName 'class.jar' }
    • 直接將編譯好的結果,push到手機上
      這里再寫一個push的task,并把將其依賴于classex_jar任務。這樣運行它時,會先去運行我們依賴的classex_jar為我們打包。
    //將編譯好的push到手機上 //這里,因為是自己定義的項目類型。要把執行的代碼,寫在Action內 //直接跟在后面的這個閉包,是在項目配置階段運行的 task adb_push(dependsOn: "classex_jar") {//doLast是定義個Action。Action是在task運行階段運行的doLast {File file = new File("./app/build/libs/class.jar")def jarDir = file.getAbsolutePath()def cmd = "adb push $jarDir /data/local/tmp"ProcessBuilder builder = new ProcessBuilder(cmd.split(" "))Process push = builder.start()if (push.waitFor() == 0) {println "result = " + push.inputStream.text} else {println "error = " + push.errorStream.text}} }
    • 直接在項目里運行調試
      我們也可以直接在項目將調試的代碼運行起來。
    //直接運行,查看結果 task adb_exc(dependsOn: "adb_push") {//相當于制定了一個Action,在這個任務的最后執行doLast {println "adb_exc"def cmd = "adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld"ProcessBuilder builder = new ProcessBuilder(cmd.split(" "))Process adbpb = builder.start()println "start adb "if (adbpb.waitFor() == 0) {println "result = " + adbpb.inputStream.text} else {println "error = " + adbpb.errorStream.text}} }

    運行結果

    ?

    運行任務.png

    基于gradle任務快速調試.png

    這樣,就把我們就可以在AndroidStudio中快速的運行調試我們的代碼了~~

    3. 通過Android程序,獲取設備的信息和錄制數據

    3.1 獲取設備信息

    • 獲取ServiceManager
      我們平時通過Context中暴露的getService方法。來調用對應的Service 來獲取設備信息的。
      因為我們是后臺運行的程序,沒有對應的Context。那我們要怎么辦?
      我們知道Android的系統架構中,其實所有的getService方法,最后都是落實在ServerManager這個代理類中,去獲取Service ManagerService中對應的真實注冊的Service的遠程代理對象。
      所以這里,我們就通過反射,來創建ServiceManager,同時通過它,來獲取我們需要的Service的遠程代理對象。
    @SuppressLint("PrivateApi") public final class ServiceManager {private final Method getServiceMethod;private DisplayManager displayManager;private PowerManager powerManager;private InputManager inputManager;public ServiceManager() {try {getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);} catch (Exception e) {e.printStackTrace();throw new AssertionError(e);}}private IInterface getService(String service, String type) {try {IBinder binder = (IBinder) getServiceMethod.invoke(null, service);Method asInterface = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);return (IInterface) asInterface.invoke(null, binder);} catch (Exception e) {e.printStackTrace();throw new AssertionError(e);}}public DisplayManager getDisplayManager() {if (displayManager == null) {IInterface service = getService(Context.DISPLAY_SERVICE, "android.hardware.display.IDisplayManager");displayManager = new DisplayManager(service);}return displayManager;}public PowerManager getPowerManager() {if (powerManager == null) {IInterface service = getService(Context.POWER_SERVICE, "android.os.IPowerManager");powerManager = new PowerManager(service);}return powerManager;}public InputManager getInputManager() {if (inputManager == null) {IInterface service = getService(Context.INPUT_SERVICE, "android.hardware.input.IInputManager");inputManager = new InputManager(service);}return inputManager;}}

    通過getService方法,獲取BINDER對象之后,在通過對應的實現類的Stub的asInterface方法,轉成對應的遠程代理類。

    • 獲取屏幕信息
      接下來,我們通過得到的遠程代理對象,就可以調用方法了
    public class DisplayManager {/*** 這個service 對應 final class BinderService extends IDisplayManager.Stub*/private final IInterface service;public DisplayManager(IInterface service) {this.service = service;}public DisplayInfo getDisplayInfo() {try {Object displayInfo = service.getClass().getMethod("getDisplayInfo", int.class).invoke(service, 0);Class<?> cls = displayInfo.getClass();// width and height already take the rotation into accountint width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);return new DisplayInfo(new Size(width, height), rotation);} catch (Exception e) {e.printStackTrace();throw new AssertionError(e);}}/*這方法是在DisplayManager里面有,但是DisplayManagerService內,沒有。所以沒法調用public DisplayInfo getDisplay() {try {Object display = service.getClass().getMethod("getDisplay", int.class).invoke(service, 0);Point point = new Point();Method getSize = display.getClass().getMethod("getSize", Point.class);Method getRotation = display.getClass().getMethod("getRotation");getSize.invoke(display, point);int rotation = (int) getRotation.invoke(display);return new DisplayInfo(new Size(point.x, point.y), rotation);} catch (Exception e) {e.printStackTrace();throw new AssertionError(e);}}*/ }

    3.2 進行屏幕錄制

    前幾遍文章,我們都是通過MediaProjection來完成我們的屏幕錄制的。
    因為截屏需要MediaProjection這個類。它實際上是一個Serivce

    //在Activity中是通過這樣的方式,來獲取VirtualDisplay的MediaProjectionManager systemService = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);MediaProjection mediaProjection = systemService.getMediaProjection();mediaProjection.createVirtualDisplay("activity-request", widht, height, 1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);

    在Activity中實際上的代碼是這樣的。通過MediaProjectionManager來獲取一個MediaProjection。然后根據我們的surface來創建一個virtualDisplay。
    整個步驟中,需要context來獲取MediaProjectionManager ,還需要用戶授權之后,才能創建VirtualDisplay。

    然而,我們這里既不可能讓用戶授權,也沒有Context,要怎么辦呢?

    SurfaceControl

    我們可以參考 adb screenrecord 命令項目中 的 ScreenRecord的操作

    /** Configures the virtual display. When this completes, virtual display* frames will start arriving from the buffer producer.*/ static status_t prepareVirtualDisplay(const DisplayInfo& mainDpyInfo,const sp<IGraphicBufferProducer>& bufferProducer,sp<IBinder>* pDisplayHandle) {sp<IBinder> dpy = SurfaceComposerClient::createDisplay(String8("ScreenRecorder"), false /*secure*/);SurfaceComposerClient::Transaction t;t.setDisplaySurface(dpy, bufferProducer);setDisplayProjection(t, dpy, mainDpyInfo);t.setDisplayLayerStack(dpy, 0); // default stackt.apply();*pDisplayHandle = dpy;return NO_ERROR; }

    Native中的SurfaceComposerClient在Java層中,對應的就是SurfaceControl。
    我們只要同樣按照這樣的方式調用SurfaceControl就可以了。

    • 獲取SurfaceControl
      同樣可以通過反射的方式來進行獲取
    @SuppressLint("PrivateApi") public class SurfaceControl {private static final Class<?> CLASS;static {try {CLASS = Class.forName("android.view.SurfaceControl");} catch (ClassNotFoundException e) {throw new AssertionError(e);}}private SurfaceControl() {// only static methods}public static void openTransaction() {try {CLASS.getMethod("openTransaction").invoke(null);} catch (Exception e) {throw new AssertionError(e);}}public static void closeTransaction() {try {CLASS.getMethod("closeTransaction").invoke(null);} catch (Exception e) {throw new AssertionError(e);}}public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {try {CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class).invoke(null, displayToken, orientation, layerStackRect, displayRect);} catch (Exception e) {throw new AssertionError(e);}}public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {try {CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);} catch (Exception e) {throw new AssertionError(e);}}public static void setDisplaySurface(IBinder displayToken, Surface surface) {try {CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);} catch (Exception e) {throw new AssertionError(e);}}public static IBinder createDisplay(String name, boolean secure) {try {return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);} catch (Exception e) {throw new AssertionError(e);}}public static void destroyDisplay(IBinder displayToken) {try {CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);} catch (Exception e) {e.printStackTrace();throw new AssertionError(e);}} }
    • 調用錄屏
    public void streamScreen(){ IBinder display = createDisplay(); //... setDisplaySurface(display, surface, contentRect, videoRect); } private static IBinder createDisplay() {return SurfaceControl.createDisplay("scrcpy", false);} private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {SurfaceControl.openTransaction();try {SurfaceControl.setDisplaySurface(display, surface);SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);SurfaceControl.setDisplayLayerStack(display, 0);} finally {SurfaceControl.closeTransaction();}}
    • 進行錄制的代碼
      其實我們已經經歷過很多很多次了。
      將錄制的數據輸入MediaCodec的Surface中。然后就可以得到編碼之后的的數據了。
      再將這個數據通過socket發送
    package com.cry.cry.appprocessdemo;import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.IBinder; import android.view.Surface;import com.cry.cry.appprocessdemo.refect.SurfaceControl;import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer;public class ScreenRecorder {private static final int DEFAULT_FRAME_RATE = 60; // fpsprivate static final int DEFAULT_I_FRAME_INTERVAL = 10; // secondsprivate static final int DEFAULT_BIT_RATE = 8000000; // 8Mbpsprivate static final int DEFAULT_TIME_OUT = 2 * 1000; // 2sprivate static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 framesprivate static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;private static final int NO_PTS = -1;private boolean sendFrameMeta = false;private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);private long ptsOrigin;private volatile boolean stop;private MediaCodec encoder;public void setStop(boolean stop) {this.stop = stop; // encoder.signalEndOfInputStream();}//進行錄制的循環,錄制得到的數據,都寫到fd當中public void record(int width, int height, FileDescriptor fd) {//對MediaCodec進行配置boolean alive;try {do {MediaFormat mediaFormat = createMediaFormat(DEFAULT_BIT_RATE, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);//輸入輸出的surface 這里是沒有encoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);Surface inputSurface = encoder.createInputSurface();IBinder surfaceClient = setDisplaySurface(width, height, inputSurface);encoder.start();try {alive = encode(encoder, fd);alive = alive && !stop;System.out.println("alive =" + alive + ", stop=" + stop);} finally {System.out.println("encoder.stop");//為什么調用stop會block主呢? // encoder.stop();System.out.println("destroyDisplaySurface");destroyDisplaySurface(surfaceClient);System.out.println("encoder release");encoder.release();System.out.println("inputSurface release");inputSurface.release();System.out.println("end");}} while (alive);} catch (IOException e) {e.printStackTrace();}System.out.println("end record");}//創建錄制的Surfaceprivate IBinder setDisplaySurface(int width, int height, Surface inputSurface) {Rect deviceRect = new Rect(0, 0, width, height);Rect displayRect = new Rect(0, 0, width, height);IBinder surfaceClient = SurfaceControl.createDisplay("recorder", false);//設置和配置截屏的SurfaceSurfaceControl.openTransaction();try {SurfaceControl.setDisplaySurface(surfaceClient, inputSurface);SurfaceControl.setDisplayProjection(surfaceClient, 0, deviceRect, displayRect);SurfaceControl.setDisplayLayerStack(surfaceClient, 0);} finally {SurfaceControl.closeTransaction();}return surfaceClient;}private void destroyDisplaySurface(IBinder surfaceClient) {SurfaceControl.destroyDisplay(surfaceClient);}//創建MediaFormatprivate MediaFormat createMediaFormat(int bitRate, int frameRate, int iFrameInterval) {MediaFormat mediaFormat = new MediaFormat();mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);mediaFormat.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate);//usreturn mediaFormat;}//進行encodeprivate boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {System.out.println("encode");boolean eof = false;MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();while (!eof) {System.out.println("dequeueOutputBuffer outputBufferId before");int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIME_OUT);System.out.println("dequeueOutputBuffer outputBufferId =" + outputBufferId);eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;System.out.println("encode eof =" + eof);try { // if (consumeRotationChange()) { // // must restart encoding with new size // break; // }if (stop) {// must restart encoding with new sizebreak;}//將得到的數據,都發送給fdif (outputBufferId >= 0) {ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);System.out.println("dequeueOutputBuffer getOutputBuffer");if (sendFrameMeta) {writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());}IO.writeFully(fd, codecBuffer);System.out.println("writeFully");}} finally {if (outputBufferId >= 0) {codec.releaseOutputBuffer(outputBufferId, false);System.out.println("releaseOutputBuffer");}}}return !eof;}private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {headerBuffer.clear();long pts;if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {pts = NO_PTS; // non-media data packet} else {if (ptsOrigin == 0) {ptsOrigin = bufferInfo.presentationTimeUs;}pts = bufferInfo.presentationTimeUs - ptsOrigin;}headerBuffer.putLong(pts);headerBuffer.putInt(packetSize);headerBuffer.flip();IO.writeFully(fd, headerBuffer);} }
    • socket發送
      調用了Os.write方法進行發送。
    public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so// count the remaining bytes manually.// See <https://github.com/Genymobile/scrcpy/issues/291>.int remaining = from.remaining();while (remaining > 0) {try {int w = Os.write(fd, from);if (BuildConfig.DEBUG && w < 0) {// w should not be negative, since an exception is thrown on errorSystem.out.println("Os.write() returned a negative value (" + w + ")");throw new AssertionError("Os.write() returned a negative value (" + w + ")");}remaining -= w;} catch (ErrnoException e) {e.printStackTrace();if (e.errno != OsConstants.EINTR) {throw new IOException(e);}}}}

    4. PC端建立Socket Client,接受數據。并將數據顯示出來

    4.1 建立Socket.

    //創建Socketclient = socket(PF_INET, SOCK_STREAM, 0);if (!client) {perror("can not create socket!!");return -1;}struct sockaddr_in in_addr;memset(&in_addr, 0, sizeof(sockaddr_in));in_addr.sin_port = htons(9000);in_addr.sin_family = AF_INET;in_addr.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = connect(client, (struct sockaddr *) &in_addr, sizeof(struct sockaddr));if (ret < 0) {perror("socket connect error!!\\n");return -1;}printf("連接成功\n");

    因為我們將PC的端口的9000轉發到Android的Server上,所以我們只要去連接本地的9000端口,就相當于和Android上的Server建立了連接。

    4.2 FFmepg解碼和SDL2顯示

    在前面的其他系列文章中,對SDL2和FFmepg都進行過了介紹。而且還對ffplay的源碼進行了分析。這里基本上和ffplay 的視頻播放功能一樣。只是我們沒有傳輸音頻數據。

    FFmepg解碼

    這里和之前的FFmpeg解碼不同的是,

    • 從內存中讀取數據
      我們不是通過一個url來獲取數據,而是通過socket的讀取內存來進行讀取數據。
      所以我們需要自己來構造這個 AVIOContext 并把它給AVFormat
    //通過這個方法,來進行socket的內存讀取 int read_socket_buffer(void *opaque, uint8_t *buf, int buf_size) {int count = recv(client, buf, buf_size, 0);if (count == 0) {return -1;}return count; }play(){avformat_network_init();AVFormatContext *format_ctx = avformat_alloc_context();unsigned char *buffer = static_cast<unsigned char *>(av_malloc(BUF_SIZE));//通過avio_alloc_context傳入內存讀取的地址和方法。AVIOContext *avio_ctx = avio_alloc_context(buffer, BUF_SIZE, 0, NULL, read_socket_buffer, NULL, NULL);//在給format_ctx 對象format_ctx->pb = avio_ctx;//最后在通過相同的方法打開ret = avformat_open_input(&format_ctx, NULL, NULL, NULL);if (ret < 0) {printf("avformat_open_input error:%s\n", av_err2str(ret));return -1;} //... }
    • 不使用avformat_find_stream_info
      因為直接發送了H264 的naul,而且沒有每次都發送媒體數據,所以當我們使用avformat_find_stream_info時,會一直阻塞,獲取不到。
      所以這里只需要直接創建解碼器,進行read_frame就可以了~~
    AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);if (!codec) {printf("Did not find a video codec \n");return -1;}AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);if (!codec_ctx) {printf("Did not alloc AVCodecContext \n");return -1;} // avcodec_parameters_to_context(codec_ctx, video_stream->codecpar); // width=1080, height=1920//這里的解碼器的長寬,只有在read_frame 之后,才能正確獲取。我們這里就先死。// 最后的項目,應該是先通過Android發送屏幕數據過來的。codec_ctx->width = 1080 / 2;codec_ctx->width = 720 / 2;codec_ctx->height = 1920 / 2;codec_ctx->height = 1280 / 2;ret = avcodec_open2(codec_ctx, codec, NULL);if (ret < 0) {printf("avcodec_open2 error:%s\n", av_err2str(ret));return -1;}printf("成功打開編碼器\n");

    SDL2顯示

    我們將解碼后的數據,送入編碼器進行顯示就可以了

    • 創建SDLScreen
    //創建SDLScreen.SDL_Screen *sc = new SDL_Screen("memory", codec_ctx->width, codec_ctx->height);ret = sc->init();if (ret < 0) {printf("SDL_Screen init error\n");return -1;}

    這里需要注意,其實這里的屏幕尺寸也是需要通過計算的。這里就簡單寫死了。

    • 簡單的編碼循環后,送入顯示
    AVFrame *pFrame = av_frame_alloc();//對比使用av_init_packet 它必須已經為packet初始化好了內存,只是設置默認值。AVPacket *packet = av_packet_alloc();while (av_read_frame(format_ctx, packet) >= 0) { // printf("av_read_frame success\n");printf("widht=%d\n", codec_ctx->width); // if (packet->stream_index == video_index) {while (1) {ret = avcodec_send_packet(codec_ctx, packet);if (ret == 0) { // printf("avcodec_send_packet success\n");//成功找到了break;} else if (ret == AVERROR(EAGAIN)) { // printf("avcodec_send_packet EAGAIN\n");break;} else {printf("avcodec_send_packet error:%s\n", av_err2str(ret));av_packet_unref(packet);goto quit;}} // while (1) {ret = avcodec_receive_frame(codec_ctx, pFrame);if (ret == 0) {//成功找到了 // printf("avcodec_receive_frame success\n"); // break;} else if (ret == AVERROR(EAGAIN)) { // printf("avcodec_receive_frame EAGAIN\n"); // break;} else {printf("avcodec_receive_frame error:%s\n", av_err2str(ret));goto quit;} // }//送現sc->send_frame(pFrame);//如果已經讀完,就GGif (avio_ctx->eof_reached) {break;} // }av_packet_unref(packet);}quit:if (client >= 0) {close(client);client = 0;}avformat_close_input(&format_ctx);sc->destroy();return 0; }

    這樣,我們就初步完成了PC的投屏功能了。

    額外-開發環境

    • mac上SDL2和FFmpeg開發環境的搭建
      因為在Clion中進行開發,所以只要簡單的配置CmakeList.txt就可以了。
    cmake_minimum_required(VERSION 3.13) project(SDLDemo)set(CMAKE_CXX_STANDARD 14)include_directories(/usr/local/Cellar/ffmpeg/4.0.3/include/) link_directories(/usr/local/Cellar/ffmpeg/4.0.3/lib/)include_directories(/usr/local/Cellar/sdl2/2.0.8/include/) link_directories(/usr/local/Cellar/sdl2/2.0.8/lib/)set(SOURCE_FILES main.cpp MSPlayer.cpp MSPlayer.h )add_executable(SDLDemo ${SOURCE_FILES})target_link_libraries(SDLDemoavcodecavdeviceavfilteravformatavresampleavutilpostprocswresampleswscaleSDL2 )

    運行

  • Android手機通過USB連接電腦,開啟USB調試模式
  • 在Studio 的項目中。運行gradle中的 adb_forward 和adb_push 任務。

    adb forward result.png

  • adb_push result.png

  • 進入adb shell 運行app_process
  • adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld

    app_process.png

  • 然后點擊運行PC上的項目,就可以看到彈出的屏幕了。
  • 總結

    通過上述的操作,我們通過USB和ADB命令,結合SDL2的提供的API和FFMpeg解碼實現了顯示。從而基本實現了PC投屏。

    但是還是存在缺陷

  • 屏幕的尺寸是我們寫死的。在不同分辨率的手機上需要每次都進行調整,才能顯示正常。
  • 我們還期望能夠通過PC來對手機進行控制
  • 目前直接在主線程中進行解碼和顯示,因為解碼的延遲,很快就能感到屏幕和手機上的延遲越來越大。
  • 投屏嘗試系列文章

    • Android PC投屏簡單嘗試- 自定義協議章(Socket+Bitmap)
    • Android PC投屏簡單嘗試(錄屏直播)2—硬解章(MediaCodec+RMTP)
    • Android PC投屏簡單嘗試(錄屏直播)3—軟解章(ImageReader+FFMpeg with X264)

    ?



    作者:deep_sadness
    鏈接:https://www.jianshu.com/p/06f4b8919991
    來源:簡書
    簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。

    總結

    以上是生活随笔為你收集整理的Android PC投屏简单尝试—最终章1的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。