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

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

上一章中,我们简单实现了PC的投屏功能。
但是还是存在这一些缺陷。

  1. 屏幕的尺寸数据是写死的
  2. 不能通过PC来对手机进行控制
  3. 直接在主线程中进行解码和显示,存在较大的延迟。

所以这边文章。我们需要根据上面的需求。来对我们的代码进行优化。

1. 屏幕信息发送

其实在上一章中,我们已经获取了屏幕信息。只是没有发送给client端。这边文章中,我们进行发送。

  • android端
    Android端在Socket连接成功后,就开启发送
    private static void sendScreenInfo(Size size, ByteBuffer buffer, FileDescriptor fileDescriptor) throws IOException {
        //将尺寸数据先发送过去
        int width = size.getWidth();
        int height = size.getHeight();
        byte wHigh = (byte) (width >> 8);
        byte wLow = (byte) (width & 0xff);

        byte hHigh = (byte) (height >> 8);
        byte hLow = (byte) (height & 0xff);

        buffer.put(wHigh);
        buffer.put(wLow);

        buffer.put(hHigh);
        buffer.put(hLow);

//            System.out.println("发送尺寸 size result = " + write);
//            int write = Os.write(fileDescriptor, buffer);
        byte[] buffer_size = new byte[4];
        buffer_size[0] = (byte) (width >> 8);
        buffer_size[1] = (byte) (width & 0xff);
        buffer_size[2] = (byte) (height >> 8);
        buffer_size[3] = (byte) (height & 0xff);
        writeFully(fileDescriptor, buffer_size, 0, buffer_size.length);
        System.out.println("发送尺寸 size result ");
        buffer.clear();
    }
  • Client端
    在PC上负责接受,并设置给编码器
  //从客户端接受屏幕数据
    uint8_t size[4];
    socketConnection->recv_from_(reinterpret_cast<uint8_t *>(size), 4);

    //这里先写死,后面从客户端内接受
    int width = (size[0] << 8) | (size[1]);
    int height = (size[2] << 8) | (size[3]);

    printf("width = %d , height = %d \n", width, height);

这样就可以获得屏幕的尺寸信息,保证不同手机分辨率也能正常使用了。

  • 奇怪的地方

     

    有点胖.png

尽管我们通过这样获取了正确的屏幕信息,但是SDL显示的画面,还是有些奇怪。比我们预期的胖了一点。

通过下面的方式,来重新计算窗口的尺寸。这样才能显示正常。

//这里是给四周留空隙。
#define DISPLAY_MARGINS 96
struct size {
    int width;
    int height;
};
// get the preferred display bounds (i.e. the screen bounds with some margins)
static SDL_bool get_preferred_display_bounds(struct size *bounds) {
    SDL_Rect rect;
#if SDL_VERSION_ATLEAST(2, 0, 5)
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
#else
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r))
#endif
    //获取显示的大小
    if (GET_DISPLAY_BOUNDS(0, &rect)) {
//        LOGW("Could not get display usable bounds: %s", SDL_GetError());
        printf("Could not get display usable bounds: %s\n", SDL_GetError());
        return SDL_FALSE;
    }
    //设置大小
    bounds->width = MAX(0, rect.w - DISPLAY_MARGINS);
    bounds->height = MAX(0, rect.h - DISPLAY_MARGINS);
    return SDL_TRUE;
}

// return the optimal size of the window, with the following constraints:
//  - it attempts to keep at least one dimension of the current_size (i.e. it crops the black borders)
//  - it keeps the aspect ratio
//  - it scales down to make it fit in the display_size
static struct size get_optimal_size(struct size current_size, struct size frame_size) {
    if (frame_size.width == 0 || frame_size.height == 0) {
        // avoid division by 0
        return current_size;
    }

    struct size display_size;
    // 32 bits because we need to multiply two 16 bits values
    int w;
    int h;

    if (!get_preferred_display_bounds(&display_size)) {
        // cannot get display bounds, do not constraint the size
        w = current_size.width;
        h = current_size.height;
    } else {
        w = MIN(current_size.width, display_size.width);
        h = MIN(current_size.height, display_size.height);
    }

    SDL_bool keep_width = static_cast<SDL_bool>(frame_size.width * h > frame_size.height * w);
  //缩放之后,保持长宽比
    if (keep_width) {
        // remove black borders on top and bottom
        h = frame_size.height * w / frame_size.width;
    } else {
        // remove black borders on left and right (or none at all if it already fits)
        w = frame_size.width * h / frame_size.height;
    }

    // w and h must fit into 16 bits
    SDL_assert_release(w < 0x10000 && h < 0x10000);
    return (struct size) {w, h};
}

//调用
void set(){
 struct size frame_size = {
            .height=screen_h,
            .width=screen_w
    };
    struct size window_size = get_optimal_size(frame_size, frame_size);

    //创建window
    sdl_window = SDL_CreateWindow(
            name,
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
            window_size.width, window_size.height,
            SDL_WINDOW_RESIZABLE);
}

这样才能显示正常的窗口了。

 

正常的比例.png

2. 对Android手机进行控制

我们知道在Android中有几种方式可以对手机的Android发起模拟按键。

  1. 通过AccessibilityService的方式。通过注册该服务,可以捕获所有的窗口变化,捕获控键,进行模拟点击。
    但是它需要额外的权限。
  2. 通过adb的方式
    我们可以简单的通过adb shell input方法来完成模拟
Usage: input [<source>] <command> [<arg>...]

The sources are: 
      dpad
      keyboard
      mouse
      touchpad
      gamepad
      touchnavigation
      joystick
      touchscreen
      stylus
      trackball

The commands and default sources are:
      text <string> (Default: touchscreen)
      keyevent [--longpress] <key code number or name> ... (Default: keyboard)
      tap <x> <y> (Default: touchscreen)
      swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
      draganddrop <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
      press (Default: trackball)
      roll <dx> <dy> (Default: trackball)

就可以对屏幕上(100,100)的位置,进行模拟点击。

  1. 通过InputManager实现
    我们这里也是通过这个方式来实现的。

InputManager 模拟点击事件

当API 15之后,我们使用InputManager。

  • 获取InputManager
    同样可以通过Server Manager中就可以进行获取。
  public InputManager getInputManager() {
        if (inputManager == null) {
            IInterface service = getService(Context.INPUT_SERVICE, "android.hardware.input.IInputManager");
            inputManager = new InputManager(service);
        }
        return inputManager;
    }

我们知道Android中的按键事件对应的是KeyEvent,而手势事件对应的是MotionEvent

  • 创建KeyEvent
public class KeyEventFactory {
    /*
    创建一个KeyEvent
     */
    public static KeyEvent keyEvent(int action, int keyCode, int repeat, int metaState) {
        long now = SystemClock.uptimeMillis();
        /**
         * 1. 点击的时间 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this key code originally went down.
         * 2. 事件发生的时间 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this event happened.
         * 3. UP DOWN MULTIPLE 中的一个: either {@link #ACTION_DOWN},{@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
         * 4. code The key code. 输入的键盘事件
         * 5. 重复的事件次数。点出次数? A repeat count for down events (> 0 if this is after the initial down) or event count for multiple events.
         * 6. metaState Flags indicating which meta keys are currently pressed.  暂时不知道什么意思
         * 7. The device ID that generated the key event.
         * 8. Raw device scan code of the event. 暂时不知道什么意思
         * 9. The flags for this key event 暂时不知道什么意思
         * 10. The input source such as {@link InputDevice#SOURCE_KEYBOARD}.
         */
        KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState,
                KeyCharacterMap.VIRTUAL_KEYBOARD,
                0,
                0,
                InputDevice.SOURCE_KEYBOARD);
        return event;
    }

    /*
    通过送入一个ACTION_DOWN 和ACTION_UP 来模拟一次点击的事件
     */
    public static KeyEvent[] clickEvent(int keyCode) {
        return new KeyEvent[]{keyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
                , keyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0)};
    }
}
  • 创建MotionEvent
    Android中的手势事件的触发。
 private static long lastMouseDown;
    private static final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
    private static final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
            .PointerProperties()};

    public static MotionEvent createMotionEvent(int type, int x, int y) {
        long now = SystemClock.uptimeMillis();
        int action;
        if (type == 1) {
            lastMouseDown = now;
            action = MotionEvent.ACTION_DOWN;
        } else {
            action = MotionEvent.ACTION_UP;
        }
        MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
        MotionEvent.PointerCoords coords = pointerCoords[0];
        coords.x = 2 * x;
        coords.y = 2 * y;
        MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
                .PointerProperties()};
        MotionEvent.PointerProperties props = pointerProperties[0];
        props.id = 0;
        props.toolType = MotionEvent.TOOL_TYPE_FINGER;

        coords = pointerCoords[0];
        coords.orientation = 0;
        coords.pressure = 1;
        coords.size = 1;

        return MotionEvent.obtain(
                lastMouseDown, now,
                action,
                1, pointerProperties, pointerCoords,
                0, 1,
                1f, 1f,
                0, 0,
                InputDevice.SOURCE_TOUCHSCREEN, 0);
    }
  • 滚动手势
public static MotionEvent createScrollEvent(int x, int y, int hScroll, int vScroll) {
        long now = SystemClock.uptimeMillis();

        MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
        MotionEvent.PointerCoords coords = pointerCoords[0];
        coords.x = 2 * x;
        coords.y = 2 * y;
        MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
                .PointerProperties()};
        MotionEvent.PointerProperties props = pointerProperties[0];
        props.id = 0;
        props.toolType = MotionEvent.TOOL_TYPE_FINGER;

        coords = pointerCoords[0];
        coords.orientation = 0;
        coords.pressure = 1;
        coords.size = 1;
        coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
        coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
        return MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0,
                0, InputDevice.SOURCE_MOUSE, 0);
    }
  • 注入Event
    最后是调用注入该事件
    public boolean injectInputEvent(InputEvent inputEvent, int mode) {
        try {
            return (Boolean) injectInputEventMethod.invoke(service, inputEvent, mode);
        } catch (InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
            throw new AssertionError(e);
        }
    }

值得注意的是:一次点击事件是由一个DOWN 和UP事件组成的。

进行通信

Client端(PC端)发送事件

通过SDL2的事件循环来监听,对输入的事件进行相应

开启事件循环

需要注意的是:

  1. 必须在主线程内(main方法所在的线程内)开启事件循环
    否则分分钟给你一个异常。
  2. 开启事件循环后,窗口上就出现按钮了

     

    开启事件循环前

     

    开启事件循环后出现窗口上的按钮.png

开启事件循环代码

  //开启Event Loop
    for (;;) {
        SDL_WaitEvent(&event);
        //这里我们主要相应了
        if (event.type == SDL_MOUSEBUTTONDOWN) {  //点击事件的DOWN
            handleButtonEvent(sc, &event.button);
        } else if (event.type == SDL_MOUSEBUTTONUP) { //点击事件的UP
            handleButtonEvent(sc, &event.button);
        } else if (event.type == SDL_KEYDOWN) {  //按键事件DOWN
            handleSDLKeyEvent(sc, &event.key);
        } else if (event.type == SDL_KEYUP) { //按键事件UP
            handleSDLKeyEvent(sc, &event.key);
        } else if (event.type == SDL_MOUSEWHEEL) {  // 滚轮事件
            //处理滑动事件
            handleScrollEvent(sc, &event.wheel);
        } else if (event.type == SDL_QUIT) {  // 点击窗口上的关闭按钮
            printf("rev event type=SDL_QUIT\n");
            sc->destroy();
            break;
        } 

事件处理代码
其实就是将这些事件解析成坐标,然后通过socket发送

//对应点击事件
void handleButtonEvent(SDL_Screen *screen, SDL_MouseButtonEvent *event) {
    int width = screen->screen_w;
    int height = screen->screen_h;
    int x = event->x;
    int y = event->y;
    //是否超过来边界
    bool outside_device_screen = x < 0 || x >= width ||
                                 y < 0 || y >= height;

    if (event->type == SDL_MOUSEBUTTONDOWN) {
    }

    printf("outside_device_screen =%d\n", outside_device_screen);
    if (outside_device_screen) {
        // ignore
        return;
    }
    char buf[6];
    memset(buf, 0, sizeof(buf));
    printf("event x =%d\n", event->x);
    printf("event y =%d\n", event->y);
    printf("event char size =%zu\n", sizeof(char));
    buf[0] = 0;
    if (event->type == SDL_MOUSEBUTTONDOWN) {
        //发送down 事件
        buf[1] = 1;
    } else {
        // 发送UP事件
        buf[1] = 0;
    }
    //高8位
    buf[2] = event->x >> 8;
    //低8位
    buf[3] = event->x & 0xff;
    //高8位
    buf[4] = event->y >> 8;
    //低8位
    buf[5] = event->y & 0xff;

    int result = send(client_event, buf, 6, 0);
    printf("send result = %d\n", result);
}

//  对应滑动事件
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer coordinates (as provided in SDL mouse events)
//
// See my question:
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
void handleScrollEvent(SDL_Screen *sc, SDL_MouseWheelEvent *event) {
    //处理滑动事件
    int x_c;
    int y_c;
    int *x = &x_c;
    int *y = &y_c;
    SDL_GetMouseState(x, y);
    SDL_Rect viewport;
    float scale_x, scale_y;
    SDL_RenderGetViewport(sc->sdl_renderer, &viewport);
    SDL_RenderGetScale(sc->sdl_renderer, &scale_x, &scale_y);
    *x = (int) (*x / scale_x) - viewport.x;
    *y = (int) (*y / scale_y) - viewport.y;


    int width = sc->screen_w;
    int height = sc->screen_h;

    //是否超过来边界
    bool outside_device_screen = x_c < 0 || x_c >= width ||
                                 y_c < 0 || y_c >= height;

    printf("outside_device_screen =%d\n", outside_device_screen);
    if (outside_device_screen) {
        // ignore
        return;
    }

    SDL_assert_release(x_c >= 0 && x_c < 0x10000 && y_c >= 0 && y_c < 0x10000);

    //使用这个来记录滑动的方向
    // SDL behavior seems inconsistent between horizontal and vertical scrolling
    // so reverse the horizontal
    // <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
    // SDL 的滑动情况,两个方向不一致
    int mul = event->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
    int hs = -mul * event->x;
    int vs = mul * event->y;

    char buf[14];
    memset(buf, 0, sizeof(buf));
    printf(" x_c =%d\n", x_c);
    printf(" y_c =%d\n", y_c);
    printf(" hs =%d\n", hs);
    printf(" vs =%d\n", vs);
    buf[0] = 0;
    //滚动事件
    buf[1] = 2;
    //高8位
    buf[2] = x_c >> 8;
    //低8位
    buf[3] = x_c & 0xff;
    //高8位
    buf[4] = y_c >> 8;
    //低8位
    buf[5] = y_c & 0xff;

    //继续滚动距离
    buf[6] = hs >> 24;
    //低8位
    buf[7] = hs >> 16;
    buf[8] = hs >> 8;
    buf[9] = hs;


    //高8位
    buf[10] = vs >> 24;
    //低8位
    buf[11] = vs >> 16;
    buf[12] = vs >> 8;
    buf[13] = vs;

    int result = send(client_event, buf, 14, 0);
    printf("send result = %d\n", result);

}

//对应键盘上的按钮事件。
void handleSDLKeyEvent(SDL_Screen *sc, SDL_KeyboardEvent *event) {
    //分别对应 mac 上的 control option command
    int ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
    int alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
    int meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
    printf("ctrl = %d,", ctrl);
    printf("meta = %d,", meta);
    printf("alt = %d,\n", alt);

    因为我是mac键盘,期望control+ H = home键 control+b = back键
    //再去取keycode
    SDL_Keycode keycode = event->keysym.sym;
    printf("keycode = %d, action type = %d\n", keycode, event->type);
    printf("b = %d, action type = %d\n", SDLK_b, event->type);
    if (event->type == SDL_KEYDOWN && ctrl != 0) {
        //这个时候发送的是按下的状态
        if (keycode == SDLK_h) {
            char buf[4];
            memset(buf, 0, sizeof(buf));
            buf[0] = 0;
            //自定义的案件事件
            buf[1] = 3;
            //1 是 down
            buf[2] = 1;
            //key code home 键对应的是 3
            buf[3] = 3;
            int result = send(client_event, buf, 4, 0);
            printf("send result = %d\n", result);
        } else if (keycode == SDLK_b) {
            char buf[4];
            memset(buf, 0, sizeof(buf));
            buf[0] = 0;
            //自定义的案件事件
            buf[1] = 3;
            //1 是 down
            buf[2] = 1;
            //key code back 键对应的是 4
            buf[3] = 4;
            int result = send(client_event, buf, 4, 0);
            printf("send result = %d\n", result);
        }
    }
    if (event->type == SDL_KEYUP && keycode != 0) {
        if (keycode == SDLK_h) {
            char buf[4];
            memset(buf, 0, sizeof(buf));
            buf[0] = 0;
            //自定义的案件事件
            buf[1] = 3;
            //1 是 up
            buf[2] = 0;
            //key code home 键对应的是 3
            buf[3] = 3;
            int result = send(client_event, buf, 4, 0);
            printf("send result = %d\n", result);
        } else if (keycode == SDLK_b) {
            char buf[4];
            memset(buf, 0, sizeof(buf));
            buf[0] = 0;
            //自定义的案件事件
            buf[1] = 3;
            //1 是 up
            buf[2] = 0;
            //key code back 键对应的是 4
            buf[3] = 4;
            int result = send(client_event, buf, 4, 0);
            printf("send result = %d\n", result);
        }
    }
}

这里可以看到,根据每一种事件,都定义了对应的方式进行发送。那Android端,可以通过对应的方式进行接收就可以了~

  • Server端(Android端)接收事件
    接收client端发送的事件。将其解析,注入
         do {
                //读到数据
                int read = Os.read(fileDescriptor, buffer);
                System.out.println("read=" + read + ",position=" + buffer.position() + "," +
                        "limit=" + buffer.limit() + ",remaining " + buffer.remaining());
                //当读到的长度为0,就结束了。
                if (read == -1 || read == 0) {
                    //如果这个时候read 0 的话。就结束
                    break;
                } else {
                    buffer.flip();
                    //上面定义的,如果是按钮事件,第一个必须是0
                    byte b = buffer.get(0);
                    //进入对应的事件
                    if (b == 0 && read > 1) { //如果是0 的话,就当作是Action
                        //第2个是判断事件的类型
                        byte type = buffer.get(1);
                       //按键事件。它发送时定义的长度是6
                        if (type < 2 && read == 6) {//action down 1 down 0 up
                            System.out.println("enter key event");
                            buffer.position(1);
                            int x = buffer.get(2) << 8 | buffer.get(3) & 0xff;
                            int y = buffer.get(4) << 8 | buffer.get(5) & 0xff;
                            //接受到事件进行处理
                            boolean key = createKey(serviceManager, type, x, y);
                            buffer.clear();
                        } else if (type == 2 && read == 14) { //滚动事件.定义的长度是14
                            buffer.position(1);
                            //x,y是接触的点,hs是水平的滑动,vs 是上下的滑动
                            int x = buffer.get(2) << 8 | buffer.get(3) & 0xff;
                            int y = buffer.get(4) << 8 | buffer.get(5) & 0xff;
                            int hs = buffer.get(6) << 24 | buffer.get(7) << 16 | buffer.get(8) <<
                                    8 | buffer.get(9);

                            int vs = buffer.get(10) << 24 | buffer.get(11) << 16 | buffer.get(12) <<
                                    8 | buffer.get(13);
                            //接受到事件进行处理
                            boolean b1 = injectScroll(serviceManager, x, y, hs, vs);
                            // 处理完,记得清楚buffer
                            buffer.clear();
                        } else if (type == 3 && read == 4) { //接受按键事件,长度是4
                            System.out.println("enter key code event");
                            int action = buffer.get(2) == 1 ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
                            int keyCode = buffer.get(3);
                            boolean key = injectKeyEvent(serviceManager, action, keyCode);
                            // 处理完,记得清楚buffer
                            buffer.clear();
                        }
                    }
                }
            } while (!eof);

这样就可以进行事件的相应了。

显示和处理事件的优化

梳理优化逻辑

  1. 解码线程异步
    虽然我们已经通过Android的Api实现了按键注入,并且定义了Socket两端对按键通信的协议。但是我们之前将解码的循环已经写在主线程中了。这样我们需要将事件的循环加入到主线程中,才能对事件发起响应。
    所以我们需要为我们的解码循环,创建一个解码线程,在异步进行解码。
  2. Socket通信异步
    同时,和上一章相同,结合我们丰富的开发经验知道,我们不能将耗时任务,放在主线程当中。所以事件通信。我们也需要放到异步处理。
  3. 队列操作
    我们知道事件循环会源源不断的送入,而我们的事件发送只能一个一个的发送。所以我们需要为事件循环加入队列的缓存。从主线程中接受事件,从发送线程中,对队列中的事件进行一个一个的处理。
    同时,根据之前的学习,我们也知道,我们的ffmpeg解码和显示其实也应该加入队列显示。这样我们就可以防止丢帧的存在。
    但是我们这里为了简单显示,只是缓存了两帧。
    一帧负责送显。一帧负责接受解码的帧。

线程模型

优化后的线程模型如下:

- client端(PC)
    - event_loop
        SDL的EventLoop。复制渲染上屏和分发事件
    - event_sender(Socket send)
        接受SDL分发的事件。并把对应的事件通过Socket分发给Android手机。
    - screen_receiver(Socket recv)
        通过Socket接受的 H264 Naul,使用FFmpeg进行解码。

- server端(Android)
    - screen record (Socket InputStream)
        使用SurfaceControl和MediaCodec进行屏幕录制,录制的结果通过Socket发送
    - event_loop (Socket OutputStream)
        接受Socket发送过来的事件。并调用对应的API进行事件的注入(InputManager)

### 线程通信
- frames
两块缓存区域。
   - decode_frame
        解码放置的frame
   - render_frame
        渲染需要的frame.使用该frame 进行render
数据流动
   - 生产的过程
     screen_receiver 负责生产。
   - 消费的过程
     event_loop 负责消费。将两块缓存区域进行交换,并把render_frame上屏

- event
一个event_queue队列来接受。可以使用链表
数据流动
   - 生产的过程
     event_loop 负责生产。并把数据送入队列当中
   - 消费的过程
     event_sender 负责消费。如果队列不为空,则进行发送

这里就不详细说明了。具体可以看代码就明白了。

最后的结果

 

最后的结果.gif


就和Vysorscrcpy一样,我们可以通过投屏PC ,并操作手机了。而且在很低的延迟下。

 

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

还有更多的细节处理,可以参考scrcpy

总结

Android PC投屏简单尝试 这一系列文章,终于到了尾声。总共横跨了大半年的事件。
最后分成下面几个方面来进行一下总结

数据源

截屏数据的获取

  1. Android的MediaProjection API
    通过MediaProjection的权限的获取和调用其API就能创建一个屏幕的录制屏幕
  2. 直接反射调用SurfaceControl的系列方法
    因为在app_process下,我们有较高的权限。所以可以直接通过反射调用SurfaceControl
    的方法,来完成录制屏幕数据的获取。(参考adb screenrecord 命令)

截屏数据的处理

  1. MediaCodec硬件编码
    使用MediaCodec结合Surface ,能容易就能得到编码后的H264数据。
  2. 使用ImageReader的方式。
    使用ImageReader 的方式,可以获取一帧一帧的数据。之后我们可以选择直接发送Bitmap数据。或者结合自己的软件解码器(FFmpeg或者X264)来编码获得H264数据。

发送的协议

自己定义的Socket协议

就是适合简单的发送Bitmap。只要接受端能够解析这个bitmap数据,就可以完成数据的展示。

RTMP协议

可以通过在服务端建立RTMP协议,然后通过这个协议进行。使用RTMP协议发送的好处在于,需要播放的端只要支持该协议,就可以轻松的进行拉流播放。

通过USB和ADB协议进行连接

这个仅仅适合于PC能够直接用ADB和手机连接的场景。
但是在这个场景下,投屏的效果清晰,流畅,延迟很低。
暂时部分,因为直接发送H264数据,只要进行解码后,就可以进行播放了。(文章使用了SDL2的方式进行了方便的播放。)

知识点

整个过程中
我们对Media Codec和ImageReader/RTMP协议/FFmpeg/SDL2/Gradle进行了知识点的串联。
其实还是挺好玩的。

另外

如果是需要改成手机和手机连接。我们要怎么实现呢?
其实从上面不难看出。如果是手机和手机连接。
在近距离,我们可以简单的使用蓝牙进行Socket(类似ADB和USB的通信方式)。
如果是远距离,就可以通过RMTP的方式,来进行推流和拉流。

最后,完结撒花?~~

投屏尝试系列文章

 



作者:deep_sadness
链接:https://www.jianshu.com/p/c2da5174d5f7
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值