回顾之前的几遍文章,我们分别通过RMTP协议和简单的Socket 发送Bitmap图片的Base64编码来完成投屏。
回想这系列文章的想法来源-Vysor,它通过 USB来进行连接的。又看到了 scrcpy项目。
于是有了这个系列的*终章-仿scrcpy(Vysor)
ps:其实就是对着scrcpy的源码撸了一遍。

效果预览

%title插图%num

简单的录制效果.gif

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

内容目录

包括的内容有

  1. 通过USB连接和adb进行手机通信
  2. 在Android端发送录制屏幕的H264 Naul
  3. 使用SDL2和FFmpeg,编写能够在PC(Windows,Mac)上运行的投屏界面

1. USB Socket连接

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

%title插图%num

adb forward.png

  1. forward –list list all forward socket connections
  2. forward [–no-rebind] LOCAL REMOTE
  3. forward socket connection using:
  4. tcp:<port> (<local> may be “tcp:0” to pick any open port)
  5. localabstract:<unix domain socket name>
  6. localreserved:<unix domain socket name>
  7. localfilesystem:<unix domain socket name>
  8. dev:<character device name>
  9. jdwp:<process pid> (remote only)
  10. forward –remove LOCAL remove specific forward socket connection
  11. forward –remove-all remove all forward socket connections

这个命令的意思是 将PC上的端口(LOCAL) 转发到 Android手机上(REMOTE) 。
这样的话,我们就可以在Android段建立Server,监听我们的REMOTE,而PC端可以通过连接连接这个LOCAL,就可以成功的建立Socket连接。
还可以注意到,我们使用的LOCALREMOTE除了可以使用 TCP的端口的形式,还可以使用 UNIX Domain Socket IPC协议 。

通常我们可以使用
adb forward tcp:8888 tcp:8888来监听两端的端口。
下面是简单的调试代码

调试代码

下面通过两种方式来进行通信

使用 tcp port的方式

  • 命令行
    adb forward tcp:8888 tcp:8888
  • socket Server(android 端)
  1. public class PortServer {
  2. public static void start() {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. //可以直接使用抽象的名字作为socket的名称
  7. ServerSocket serverSocket = null;
  8. try {
  9. serverSocket = new ServerSocket(8888);
  10. //blocking
  11. Socket accept = serverSocket.accept();
  12. Log.d(“ZZX”, “serverSocket 链接成功”);
  13. while (true) {
  14. if (!accept.isConnected()) {
  15. return;
  16. }
  17. InputStream inputStream = accept.getInputStream();
  18. String result = IO.streamToString(inputStream);
  19. Log.d(“ZZX”, “serverSocket recv =” + result);
  20. }
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }).start();
  28. }
  29. }
  • socket client 端
    java 版本
  1. private static void socket() {
  2. //开启socket clinet
  3. Socket clinet = new Socket();
  4. //用adb转发端口到8888
  5. try {
  6. //blocking
  7. clinet.connect(new InetSocketAddress(“127.0.0.1”, 8888));
  8. System.out.println(“连接成功!!”);
  9. OutputStream outputStream = clinet.getOutputStream();
  10. outputStream.write(“Hello World”.getBytes());
  11. outputStream.flush();
  12. outputStream.close();
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }

使用 localabstract 的方式

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

需要修改的只有Server 端。因为我们将服务端(Android端)改成了localabstract的方式。

  • android
  1. public class LocalServer {
  2. public static void start() {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. //可以直接使用抽象的名字作为socket的名称
  7. LocalServerSocket serverSocket = null;
  8. try {
  9. serverSocket = new LocalServerSocket(“local”);
  10. //blocking
  11. LocalSocket client = serverSocket.accept();
  12. Log.d(“ZZX”, “serverSocket 链接成功”);
  13. while (true) {
  14. if (!client.isConnected()) {
  15. return;
  16. }
  17. InputStream inputStream = client.getInputStream();
  18. String result = IO.streamToString(inputStream);
  19. Log.d(“ZZX”, “serverSocket recv =” + result);
  20. }
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }).start();
  28. }
  29. }

这样我们就通过ADB协议和USB 建立了手机和PC端的通信。之后我们就可以通过这里建立的socket,进行数据传递。

2. 在Android上运行后台程序

我们期望运行一个后台的程序,PC端开启之后,就开始给我们发送截屏的数据。
这里涉及了几个问题。
*个是在之前的文章中,我们知道,我们需要进行屏幕的截屏,需要申请对应的权限。
第二个是,如何直接在Android上运行我们写好的java程序,还不是用Activity的方式来运行。

使用app_process 运行程序

这个命令完美的满足了我们的需求。
它不但可以在后台直接运行dex中的java文件,还具有较高的权限!!!

调试代码

我们先来写一个简单的类试一下

  • 编写一个简单的java文件
  1. public class HelloWorld {
  2. public static void main(String… args) {
  3. System.out.println(“Hello, world!”);
  4. }
  5. }
  • 将其编译成dex文件
  1. javac -source 1.7 -target 1.7 HelloWorld.java
  2. $ANDROID_HOME/build-tools/27.0.2/dx \
  3. –dex –output classes.dex HelloWorld.class
  • 推送到设备上并执行它
  1. adb push classes.dex /data/local/tmp/
  2. $ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / HelloWorld

这样我们就运行成功了~~

这里需要注意的是app_process 只能执行dex文件

  • adb_forward
  1. task adb_forward() {
  2. doLast {
  3. println “adb_forward”
  4. def cmd = “adb forward –list”
  5. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  6. Process adbpb = builder.start()
  7. if (adbpb.waitFor() == 0) {
  8. def result = adbpb.inputStream.text
  9. println result.length()
  10. if (result.length() <= 1) {
  11. def forward_recorder = “adb forward tcp:9000 localabstract:recorder”
  12. ProcessBuilder forward_pb = new ProcessBuilder(forward_recorder.split(” “))
  13. Process forward_ps = forward_pb.start()
  14. if (forward_ps.waitFor() == 0) {
  15. println “forward success!!”
  16. }
  17. } else {
  18. println result
  19. }
  20. } else {
  21. println “error = ” + adbpb.errorStream.text
  22. }
  23. }
  24. }

在AndroidStudio中编译

上面我体验了简单的通过命令行的形式来编译。实际上,我们的开发都是在AndroidStudio中的。而且它为我们提供了一个很好的编译环境。不需要在手动去敲入这些代码。
我们只需要通过自定义Gradle Task就可以简单的完成这个任务。

  • 单纯复制dex文件
    因为运行的是dex文件,所以我们直接复制dex文件就行了。
    这个任务必须依赖于assembleDebug任务。因为只有这个任务执行完,才会有这些dex文件。
  1. //将dex文件复制。通过dependsOn来制定依赖关系
  2. //因为是设定了type是gradle中已经实现了Copy,所以直接配置它的属性
  3. task class_cls(type: Copy, dependsOn: “assembleDebug”) {
  4. from “build/intermediates/transforms/dexMerger/debug/0/”
  5. destinationDir = file(‘build/libs’)
  6. }
  • 压缩成jar
    如果不直接使用dex的话,也可以压缩成jar的形式。其实和上面直接使用dex的方式没差。就看你自己喜欢了。
  1. //将编译好的dex文件压缩成jar.
  2. task classex_jar(type: Jar, dependsOn: “assembleDebug”) {
  3. from “build/intermediates/transforms/dexMerger/debug/0/”
  4. destinationDir = file(‘build/libs’)
  5. archiveName ‘class.jar’
  6. }
  • 直接将编译好的结果,push到手机上
    这里再写一个push的task,并把将其依赖于classex_jar任务。这样运行它时,会先去运行我们依赖的classex_jar为我们打包。
  1. //将编译好的push到手机上
  2. //这里,因为是自己定义的项目类型。要把执行的代码,写在Action内
  3. //直接跟在后面的这个闭包,是在项目配置阶段运行的
  4. task adb_push(dependsOn: “classex_jar”) {
  5. //doLast是定义个Action。Action是在task运行阶段运行的
  6. doLast {
  7. File file = new File(“./app/build/libs/class.jar”)
  8. def jarDir = file.getAbsolutePath()
  9. def cmd = “adb push $jarDir /data/local/tmp”
  10. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  11. Process push = builder.start()
  12. if (push.waitFor() == 0) {
  13. println “result = “ + push.inputStream.text
  14. } else {
  15. println “error = “ + push.errorStream.text
  16. }
  17. }
  18. }
  • 直接在项目里运行调试
    我们也可以直接在项目将调试的代码运行起来。
  1. //直接运行,查看结果
  2. task adb_exc(dependsOn: “adb_push”) {
  3. //相当于制定了一个Action,在这个任务的*后执行
  4. doLast {
  5. println “adb_exc”
  6. def cmd = “adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld”
  7. ProcessBuilder builder = new ProcessBuilder(cmd.split(” “))
  8. Process adbpb = builder.start()
  9. println “start adb “
  10. if (adbpb.waitFor() == 0) {
  11. println “result = “ + adbpb.inputStream.text
  12. } else {
  13. println “error = “ + adbpb.errorStream.text
  14. }
  15. }
  16. }

运行结果

 

%title插图%num

运行任务.png

%title插图%num

基于gradle任务快速调试.png

这样,就把我们就可以在AndroidStudio中快速的运行调试我们的代码了~~

3. 通过Android程序,获取设备的信息和录制数据

3.1 获取设备信息

  • 获取ServiceManager
    我们平时通过Context中暴露的getService方法。来调用对应的Service 来获取设备信息的。
    因为我们是后台运行的程序,没有对应的Context。那我们要怎么办?
    我们知道Android的系统架构中,其实所有的getService方法,*后都是落实在ServerManager这个代理类中,去获取Service ManagerService中对应的真实注册的Service的远程代理对象。
    所以这里,我们就通过反射,来创建ServiceManager,同时通过它,来获取我们需要的Service的远程代理对象。
  1. @SuppressLint(“PrivateApi”)
  2. public final class ServiceManager {
  3. private final Method getServiceMethod;
  4. private DisplayManager displayManager;
  5. private PowerManager powerManager;
  6. private InputManager inputManager;
  7. public ServiceManager() {
  8. try {
  9. getServiceMethod = Class.forName(“android.os.ServiceManager”).getDeclaredMethod(“getService”, String.class);
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. throw new AssertionError(e);
  13. }
  14. }
  15. private IInterface getService(String service, String type) {
  16. try {
  17. IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
  18. Method asInterface = Class.forName(type + “$Stub”).getMethod(“asInterface”, IBinder.class);
  19. return (IInterface) asInterface.invoke(null, binder);
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. throw new AssertionError(e);
  23. }
  24. }
  25. public DisplayManager getDisplayManager() {
  26. if (displayManager == null) {
  27. IInterface service = getService(Context.DISPLAY_SERVICE, “android.hardware.display.IDisplayManager”);
  28. displayManager = new DisplayManager(service);
  29. }
  30. return displayManager;
  31. }
  32. public PowerManager getPowerManager() {
  33. if (powerManager == null) {
  34. IInterface service = getService(Context.POWER_SERVICE, “android.os.IPowerManager”);
  35. powerManager = new PowerManager(service);
  36. }
  37. return powerManager;
  38. }
  39. public InputManager getInputManager() {
  40. if (inputManager == null) {
  41. IInterface service = getService(Context.INPUT_SERVICE, “android.hardware.input.IInputManager”);
  42. inputManager = new InputManager(service);
  43. }
  44. return inputManager;
  45. }
  46. }

通过getService方法,获取BINDER对象之后,在通过对应的实现类的StubasInterface方法,转成对应的远程代理类。

  • 获取屏幕信息
    接下来,我们通过得到的远程代理对象,就可以调用方法了
  1. public class DisplayManager {
  2. /**
  3. * 这个service 对应 final class BinderService extends IDisplayManager.Stub
  4. */
  5. private final IInterface service;
  6. public DisplayManager(IInterface service) {
  7. this.service = service;
  8. }
  9. public DisplayInfo getDisplayInfo() {
  10. try {
  11. Object displayInfo = service.getClass().getMethod(“getDisplayInfo”, int.class)
  12. .invoke(service, 0);
  13. Class<?> cls = displayInfo.getClass();
  14. // width and height already take the rotation into account
  15. int width = cls.getDeclaredField(“logicalWidth”).getInt(displayInfo);
  16. int height = cls.getDeclaredField(“logicalHeight”).getInt(displayInfo);
  17. int rotation = cls.getDeclaredField(“rotation”).getInt(displayInfo);
  18. return new DisplayInfo(new Size(width, height), rotation);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. throw new AssertionError(e);
  22. }
  23. }
  24. /*
  25. 这方法是在DisplayManager里面有,但是DisplayManagerService内,没有。所以没法调用
  26. public DisplayInfo getDisplay() {
  27. try {
  28. Object display = service.getClass().getMethod(“getDisplay”, int.class)
  29. .invoke(service, 0);
  30. Point point = new Point();
  31. Method getSize = display.getClass().getMethod(“getSize”, Point.class);
  32. Method getRotation = display.getClass().getMethod(“getRotation”);
  33. getSize.invoke(display, point);
  34. int rotation = (int) getRotation.invoke(display);
  35. return new DisplayInfo(new Size(point.x, point.y), rotation);
  36. } catch (Exception e) {
  37. e.printStackTrace();
  38. throw new AssertionError(e);
  39. }
  40. }
  41. */
  42. }

3.2 进行屏幕录制

前几遍文章,我们都是通过MediaProjection来完成我们的屏幕录制的。
因为截屏需要MediaProjection这个类。它实际上是一个Serivce

  1. //在Activity中是通过这样的方式,来获取VirtualDisplay的
  2. MediaProjectionManager systemService = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
  3. MediaProjection mediaProjection = systemService.getMediaProjection();
  4. 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的操作

  1. /*
  2. * Configures the virtual display. When this completes, virtual display
  3. * frames will start arriving from the buffer producer.
  4. */
  5. static status_t prepareVirtualDisplay(const DisplayInfo& mainDpyInfo,
  6. const sp<IGraphicBufferProducer>& bufferProducer,
  7. sp<IBinder>* pDisplayHandle) {
  8. sp<IBinder> dpy = SurfaceComposerClient::createDisplay(
  9. String8(“ScreenRecorder”), false /*secure*/);
  10. SurfaceComposerClient::Transaction t;
  11. t.setDisplaySurface(dpy, bufferProducer);
  12. setDisplayProjection(t, dpy, mainDpyInfo);
  13. t.setDisplayLayerStack(dpy, 0); // default stack
  14. t.apply();
  15. *pDisplayHandle = dpy;
  16. return NO_ERROR;
  17. }

Native中的SurfaceComposerClient在Java层中,对应的就是SurfaceControl。
我们只要同样按照这样的方式调用SurfaceControl就可以了。

  • 获取SurfaceControl
    同样可以通过反射的方式来进行获取
  1. @SuppressLint(“PrivateApi”)
  2. public class SurfaceControl {
  3. private static final Class<?> CLASS;
  4. static {
  5. try {
  6. CLASS = Class.forName(“android.view.SurfaceControl“);
  7. } catch (ClassNotFoundException e) {
  8. throw new AssertionError(e);
  9. }
  10. }
  11. private SurfaceControl() {
  12. // only static methods
  13. }
  14. public static void openTransaction() {
  15. try {
  16. CLASS.getMethod(“openTransaction”).invoke(null);
  17. } catch (Exception e) {
  18. throw new AssertionError(e);
  19. }
  20. }
  21. public static void closeTransaction() {
  22. try {
  23. CLASS.getMethod(“closeTransaction”).invoke(null);
  24. } catch (Exception e) {
  25. throw new AssertionError(e);
  26. }
  27. }
  28. public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
  29. try {
  30. CLASS.getMethod(“setDisplayProjection”, IBinder.class, int.class, Rect.class, Rect.class)
  31. .invoke(null, displayToken, orientation, layerStackRect, displayRect);
  32. } catch (Exception e) {
  33. throw new AssertionError(e);
  34. }
  35. }
  36. public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
  37. try {
  38. CLASS.getMethod(“setDisplayLayerStack”, IBinder.class, int.class).invoke(null, displayToken, layerStack);
  39. } catch (Exception e) {
  40. throw new AssertionError(e);
  41. }
  42. }
  43. public static void setDisplaySurface(IBinder displayToken, Surface surface) {
  44. try {
  45. CLASS.getMethod(“setDisplaySurface”, IBinder.class, Surface.class).invoke(null, displayToken, surface);
  46. } catch (Exception e) {
  47. throw new AssertionError(e);
  48. }
  49. }
  50. public static IBinder createDisplay(String name, boolean secure) {
  51. try {
  52. return (IBinder) CLASS.getMethod(“createDisplay”, String.class, boolean.class).invoke(null, name, secure);
  53. } catch (Exception e) {
  54. throw new AssertionError(e);
  55. }
  56. }
  57. public static void destroyDisplay(IBinder displayToken) {
  58. try {
  59. CLASS.getMethod(“destroyDisplay”, IBinder.class).invoke(null, displayToken);
  60. } catch (Exception e) {
  61. e.printStackTrace();
  62. throw new AssertionError(e);
  63. }
  64. }
  65. }
  • 调用录屏
  1. public void streamScreen(){
  2. IBinder display = createDisplay();
  3. //…
  4. setDisplaySurface(display, surface, contentRect, videoRect);
  5. }
  6. private static IBinder createDisplay() {
  7. return SurfaceControl.createDisplay(“scrcpy”, false);
  8. }
  9. private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
  10. SurfaceControl.openTransaction();
  11. try {
  12. SurfaceControl.setDisplaySurface(display, surface);
  13. SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
  14. SurfaceControl.setDisplayLayerStack(display, 0);
  15. } finally {
  16. SurfaceControl.closeTransaction();
  17. }
  18. }
  • 进行录制的代码
    其实我们已经经历过很多很多次了。
    将录制的数据输入MediaCodec的Surface中。然后就可以得到编码之后的的数据了。
    再将这个数据通过socket发送
  1. package com.cry.cry.appprocessdemo;
  2. import android.graphics.Rect;
  3. import android.media.MediaCodec;
  4. import android.media.MediaCodecInfo;
  5. import android.media.MediaFormat;
  6. import android.os.IBinder;
  7. import android.view.Surface;
  8. import com.cry.cry.appprocessdemo.refect.SurfaceControl;
  9. import java.io.FileDescriptor;
  10. import java.io.IOException;
  11. import java.nio.ByteBuffer;
  12. public class ScreenRecorder {
  13. private static final int DEFAULT_FRAME_RATE = 60; // fps
  14. private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
  15. private static final int DEFAULT_BIT_RATE = 8000000; // 8Mbps
  16. private static final int DEFAULT_TIME_OUT = 2 * 1000; // 2s
  17. private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
  18. private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
  19. private static final int NO_PTS = –1;
  20. private boolean sendFrameMeta = false;
  21. private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
  22. private long ptsOrigin;
  23. private volatile boolean stop;
  24. private MediaCodec encoder;
  25. public void setStop(boolean stop) {
  26. this.stop = stop;
  27. // encoder.signalEndOfInputStream();
  28. }
  29. //进行录制的循环,录制得到的数据,都写到fd当中
  30. public void record(int width, int height, FileDescriptor fd) {
  31. //对MediaCodec进行配置
  32. boolean alive;
  33. try {
  34. do {
  35. MediaFormat mediaFormat = createMediaFormat(DEFAULT_BIT_RATE, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
  36. mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
  37. mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
  38. encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
  39. //输入输出的surface 这里是没有
  40. encoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  41. Surface inputSurface = encoder.createInputSurface();
  42. IBinder surfaceClient = setDisplaySurface(width, height, inputSurface);
  43. encoder.start();
  44. try {
  45. alive = encode(encoder, fd);
  46. alive = alive && !stop;
  47. System.out.println(“alive =” + alive + “, stop=” + stop);
  48. } finally {
  49. System.out.println(“encoder.stop”);
  50. //为什么调用stop会block主呢?
  51. // encoder.stop();
  52. System.out.println(“destroyDisplaySurface”);
  53. destroyDisplaySurface(surfaceClient);
  54. System.out.println(“encoder release”);
  55. encoder.release();
  56. System.out.println(“inputSurface release”);
  57. inputSurface.release();
  58. System.out.println(“end”);
  59. }
  60. } while (alive);
  61. } catch (IOException e) {
  62. e.printStackTrace();
  63. }
  64. System.out.println(“end record”);
  65. }
  66. //创建录制的Surface
  67. private IBinder setDisplaySurface(int width, int height, Surface inputSurface) {
  68. Rect deviceRect = new Rect(0, 0, width, height);
  69. Rect displayRect = new Rect(0, 0, width, height);
  70. IBinder surfaceClient = SurfaceControl.createDisplay(“recorder”, false);
  71. //设置和配置截屏的Surface
  72. SurfaceControl.openTransaction();
  73. try {
  74. SurfaceControl.setDisplaySurface(surfaceClient, inputSurface);
  75. SurfaceControl.setDisplayProjection(surfaceClient, 0, deviceRect, displayRect);
  76. SurfaceControl.setDisplayLayerStack(surfaceClient, 0);
  77. } finally {
  78. SurfaceControl.closeTransaction();
  79. }
  80. return surfaceClient;
  81. }
  82. private void destroyDisplaySurface(IBinder surfaceClient) {
  83. SurfaceControl.destroyDisplay(surfaceClient);
  84. }
  85. //创建MediaFormat
  86. private MediaFormat createMediaFormat(int bitRate, int frameRate, int iFrameInterval) {
  87. MediaFormat mediaFormat = new MediaFormat();
  88. mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
  89. mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
  90. mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
  91. mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
  92. mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
  93. mediaFormat.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate);//us
  94. return mediaFormat;
  95. }
  96. //进行encode
  97. private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
  98. System.out.println(“encode”);
  99. boolean eof = false;
  100. MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
  101. while (!eof) {
  102. System.out.println(“dequeueOutputBuffer outputBufferId before”);
  103. int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIME_OUT);
  104. System.out.println(“dequeueOutputBuffer outputBufferId =” + outputBufferId);
  105. eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
  106. System.out.println(“encode eof =” + eof);
  107. try {
  108. // if (consumeRotationChange()) {
  109. // // must restart encoding with new size
  110. // break;
  111. // }
  112. if (stop) {
  113. // must restart encoding with new size
  114. break;
  115. }
  116. //将得到的数据,都发送给fd
  117. if (outputBufferId >= 0) {
  118. ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
  119. System.out.println(“dequeueOutputBuffer getOutputBuffer”);
  120. if (sendFrameMeta) {
  121. writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
  122. }
  123. IO.writeFully(fd, codecBuffer);
  124. System.out.println(“writeFully”);
  125. }
  126. } finally {
  127. if (outputBufferId >= 0) {
  128. codec.releaseOutputBuffer(outputBufferId, false);
  129. System.out.println(“releaseOutputBuffer”);
  130. }
  131. }
  132. }
  133. return !eof;
  134. }
  135. private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
  136. headerBuffer.clear();
  137. long pts;
  138. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
  139. pts = NO_PTS; // non-media data packet
  140. } else {
  141. if (ptsOrigin == 0) {
  142. ptsOrigin = bufferInfo.presentationTimeUs;
  143. }
  144. pts = bufferInfo.presentationTimeUs – ptsOrigin;
  145. }
  146. headerBuffer.putLong(pts);
  147. headerBuffer.putInt(packetSize);
  148. headerBuffer.flip();
  149. IO.writeFully(fd, headerBuffer);
  150. }
  151. }
  • socket发送
    调用了Os.write方法进行发送。
  1. public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
  2. // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
  3. // count the remaining bytes manually.
  4. // See <https://github.com/Genymobile/scrcpy/issues/291>.
  5. int remaining = from.remaining();
  6. while (remaining > 0) {
  7. try {
  8. int w = Os.write(fd, from);
  9. if (BuildConfig.DEBUG && w < 0) {
  10. // w should not be negative, since an exception is thrown on error
  11. System.out.println(“Os.write() returned a negative value (“ + w + “)”);
  12. throw new AssertionError(“Os.write() returned a negative value (“ + w + “)”);
  13. }
  14. remaining -= w;
  15. } catch (ErrnoException e) {
  16. e.printStackTrace();
  17. if (e.errno != OsConstants.EINTR) {
  18. throw new IOException(e);
  19. }
  20. }
  21. }
  22. }

4. PC端建立Socket Client,接受数据。并将数据显示出来

4.1 建立Socket.

  1. //创建Socket
  2. client = socket(PF_INET, SOCK_STREAM, 0);
  3. if (!client) {
  4. perror(“can not create socket!!”);
  5. return -1;
  6. }
  7. struct sockaddr_in in_addr;
  8. memset(&in_addr, 0, sizeof(sockaddr_in));
  9. in_addr.sin_port = htons(9000);
  10. in_addr.sin_family = AF_INET;
  11. in_addr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
  12. int ret = connect(client, (struct sockaddr *) &in_addr, sizeof(struct sockaddr));
  13. if (ret < 0) {
  14. perror(“socket connect error!!\\n”);
  15. return -1;
  16. }
  17. printf(“连接成功\n”);

因为我们将PC的端口的9000转发到Android的Server上,所以我们只要去连接本地的9000端口,就相当于和Android上的Server建立了连接。

4.2 FFmepg解码和SDL2显示

在前面的其他系列文章中,对SDL2和FFmepg都进行过了介绍。而且还对ffplay的源码进行了分析。这里基本上和ffplay 的视频播放功能一样。只是我们没有传输音频数据。

FFmepg解码

这里和之前的FFmpeg解码不同的是,

  • 从内存中读取数据
    我们不是通过一个url来获取数据,而是通过socket的读取内存来进行读取数据。
    所以我们需要自己来构造这个 AVIOContext 并把它给AVFormat
  1. //通过这个方法,来进行socket的内存读取
  2. int read_socket_buffer(void *opaque, uint8_t *buf, int buf_size) {
  3. int count = recv(client, buf, buf_size, 0);
  4. if (count == 0) {
  5. return -1;
  6. }
  7. return count;
  8. }
  9. play(){
  10. avformat_network_init();
  11. AVFormatContext *format_ctx = avformat_alloc_context();
  12. unsigned char *buffer = static_cast<unsigned char *>(av_malloc(BUF_SIZE));
  13. //通过avio_alloc_context传入内存读取的地址和方法。
  14. AVIOContext *avio_ctx = avio_alloc_context(buffer, BUF_SIZE, 0, NULL, read_socket_buffer, NULL, NULL);
  15. //在给format_ctx 对象
  16. format_ctx->pb = avio_ctx;
  17. //*后在通过相同的方法打开
  18. ret = avformat_open_input(&format_ctx, NULL, NULL, NULL);
  19. if (ret < 0) {
  20. printf(“avformat_open_input error:%s\n”, av_err2str(ret));
  21. return -1;
  22. }
  23. //…
  24. }
  • 不使用avformat_find_stream_info
    因为直接发送了H264 的naul,而且没有每次都发送媒体数据,所以当我们使用avformat_find_stream_info时,会一直阻塞,获取不到。
    所以这里只需要直接创建解码器,进行read_frame就可以了~~
  1. AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
  2. if (!codec) {
  3. printf(“Did not find a video codec \n”);
  4. return -1;
  5. }
  6. AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
  7. if (!codec_ctx) {
  8. printf(“Did not alloc AVCodecContext \n”);
  9. return -1;
  10. }
  11. // avcodec_parameters_to_context(codec_ctx, video_stream->codecpar);
  12. // width=1080, height=1920
  13. //这里的解码器的长宽,只有在read_frame 之后,才能正确获取。我们这里就先死。
  14. // *后的项目,应该是先通过Android发送屏幕数据过来的。
  15. codec_ctx->width = 1080 / 2;
  16. codec_ctx->width = 720 / 2;
  17. codec_ctx->height = 1920 / 2;
  18. codec_ctx->height = 1280 / 2;
  19. ret = avcodec_open2(codec_ctx, codec, NULL);
  20. if (ret < 0) {
  21. printf(“avcodec_open2 error:%s\n”, av_err2str(ret));
  22. return -1;
  23. }
  24. printf(“成功打开编码器\n”);

SDL2显示

我们将解码后的数据,送入编码器进行显示就可以了

  • 创建SDLScreen
  1. //创建SDLScreen.
  2. SDL_Screen *sc = new SDL_Screen(“memory”, codec_ctx->width, codec_ctx->height);
  3. ret = sc->init();
  4. if (ret < 0) {
  5. printf(“SDL_Screen init error\n”);
  6. return -1;
  7. }

这里需要注意,其实这里的屏幕尺寸也是需要通过计算的。这里就简单写死了。

  • 简单的编码循环后,送入显示
  1. AVFrame *pFrame = av_frame_alloc();
  2. //对比使用av_init_packet 它必须已经为packet初始化好了内存,只是设置默认值。
  3. AVPacket *packet = av_packet_alloc();
  4. while (av_read_frame(format_ctx, packet) >= 0) {
  5. // printf(“av_read_frame success\n”);
  6. printf(“widht=%d\n”, codec_ctx->width);
  7. // if (packet->stream_index == video_index) {
  8. while (1) {
  9. ret = avcodec_send_packet(codec_ctx, packet);
  10. if (ret == 0) {
  11. // printf(“avcodec_send_packet success\n”);
  12. //成功找到了
  13. break;
  14. } else if (ret == AVERROR(EAGAIN)) {
  15. // printf(“avcodec_send_packet EAGAIN\n”);
  16. break;
  17. } else {
  18. printf(“avcodec_send_packet error:%s\n”, av_err2str(ret));
  19. av_packet_unref(packet);
  20. goto quit;
  21. }
  22. }
  23. // while (1) {
  24. ret = avcodec_receive_frame(codec_ctx, pFrame);
  25. if (ret == 0) {
  26. //成功找到了
  27. // printf(“avcodec_receive_frame success\n”);
  28. // break;
  29. } else if (ret == AVERROR(EAGAIN)) {
  30. // printf(“avcodec_receive_frame EAGAIN\n”);
  31. // break;
  32. } else {
  33. printf(“avcodec_receive_frame error:%s\n”, av_err2str(ret));
  34. goto quit;
  35. }
  36. // }
  37. //送现
  38. sc->send_frame(pFrame);
  39. //如果已经读完,就GG
  40. if (avio_ctx->eof_reached) {
  41. break;
  42. }
  43. // }
  44. av_packet_unref(packet);
  45. }
  46. quit:
  47. if (client >= 0) {
  48. close(client);
  49. client = 0;
  50. }
  51. avformat_close_input(&format_ctx);
  52. sc->destroy();
  53. return 0;
  54. }

这样,我们就初步完成了PC的投屏功能了。

额外-开发环境

  • mac上SDL2和FFmpeg开发环境的搭建
    因为在Clion中进行开发,所以只要简单的配置CmakeList.txt就可以了。
  1. cmake_minimum_required(VERSION 3.13)
  2. project(SDLDemo)
  3. set(CMAKE_CXX_STANDARD 14)
  4. include_directories(/usr/local/Cellar/ffmpeg/4.0.3/include/)
  5. link_directories(/usr/local/Cellar/ffmpeg/4.0.3/lib/)
  6. include_directories(/usr/local/Cellar/sdl2/2.0.8/include/)
  7. link_directories(/usr/local/Cellar/sdl2/2.0.8/lib/)
  8. set(SOURCE_FILES main.cpp MSPlayer.cpp MSPlayer.h )
  9. add_executable(SDLDemo ${SOURCE_FILES})
  10. target_link_libraries(
  11. SDLDemo
  12. avcodec
  13. avdevice
  14. avfilter
  15. avformat
  16. avresample
  17. avutil
  18. postproc
  19. swresample
  20. swscale
  21. SDL2
  22. )

运行

  1. Android手机通过USB连接电脑,开启USB调试模式
  2. 在Studio 的项目中。运行gradle中的 adb_forward 和adb_push 任务。%title插图%num

    adb forward result.png

%title插图%num

adb_push result.png

  1. 进入adb shell 运行app_process
adb shell CLASSPATH=/data/local/tmp/class.jar app_process /data/local/tmp com.cry.cry.appprocessdemo.HelloWorld

%title插图%num

app_process.png

  1. 然后点击运行PC上的项目,就可以看到弹出的屏幕了。

总结

通过上述的操作,我们通过USB和ADB命令,结合SDL2的提供的API和FFMpeg解码实现了显示。从而基本实现了PC投屏。

但是还是存在缺陷

  1. 屏幕的尺寸是我们写死的。在不同分辨率的手机上需要每次都进行调整,才能显示正常。
  2. 我们还期望能够通过PC来对手机进行控制
  3. 目前直接在主线程中进行解码和显示,因为解码的延迟,很快就能感到屏幕和手机上的延迟越来越大。