精华内容
下载资源
问答
  • apicloud开发源码,支持安卓+苹果IOS,千月影视V7带投屏功能,前端+后端,支持开源!
  • 一个 Docker 镜像包含所有功能?Yes!!!(如:在群晖中运行) 常见问题 FAQ Plex 快速运行 这个项目是提供一个 docker-compose 文件,让你可以一步就拥有一个集图形化界面的 Aria2 下载(AriaNg)和在线预览和管理文件...
  • java投屏源码 这个 repo 是一个带有 UI 的 Web 应用程序的入门项目。 可以建造什么? 选择这个项目作为入门项目,您可以轻松地为数据库 CRUD 操作构建一个 Web 应用程序,该应用程序具有 REST 接口、身份验证和动态...
  • 萝卜影视修复版app视频源码可对接苹果CMS 官解+下载+投屏+UI美化+功能修复 本源码就是UI美化+功能修复版本,非网上现有源码,本源码某站售价299,今天免费开放给会员下载 本源码后端为苹果cms V10程序,程序已经...

    萝卜影视修复版 app视频源码 可对接苹果CMS 官解+下载+投屏+UI美化+功能修复
    本源码就是UI美化+功能修复版本,非网上现有源码,本源码某站售价299,今天免费开放给会员下载

    本源码后端为苹果cms V10程序,程序已经二开,需更改后端文件,所以已有网站不支持直接替换,购买请慎重!本源码都是经过测试无错,遇到问题,先自行解决如遇重大问题,反应给我们,我们会统一修复原文https://bbs.td0528.com/106.html

    展开全文
  • 源码自带独家有效完美防黑功能 前端和后端 数据库都在里面了还有教程 只要有眼睛都可以搭建成功 稳定运营 安装说明: 后端替换域名app.stb789.cn 前端替换域名app.stb789.cn 软件名字:畅视影院 都替换 后台设置自己...

    介绍:

    影视app全新双端开源系统,这次没什么好说的,教程都在文件!
    可以自定义易支付接口,带投屏,带选集,对接苹果苹果CMS 10
    【程序源码】全新双端美化开源系统
    源码自带独家有效完美防黑功能
    前端和后端 数据库都在里面了还有教程
    只要有眼睛都可以搭建成功 稳定运营

    安装说明:
    后端替换域名app.stb789.cn
    前端替换域名app.stb789.cn
    软件名字:畅视影院 都替换

    后台设置自己看情况使用,有些功能或者说明是没用的。

    后台路径:域名/login/login

    管理登录admin  123456


    网盘下载地址:

    https://zijiewangpan.com/HuMFsmz5aLV


    图片:


    展开全文
  • Android PC投屏简单尝试—最终章2

    千次阅读 2019-01-08 18:05:10
    上一章中,我们简单实现了PC的投屏功能。 但是还是存在这一些缺陷。 屏幕的尺寸数据是写死的 不能通过PC来对手机进行控制 直接在主线程中进行解码和显示,存在较大的延迟。 所以这边文章。我们需要根据上面的需求...

    源码地址: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.启动后加载谷歌地图默认定位到中国地图; 2.选择坐标位置可以添加marker并显示自定义...4.连接多台显示器可以实现投屏功能; 5.附件为完整源码工程; 6.为什么csdn资源的积分变的不能自定义了,积分好少啊。。。
  • 畅视影视源码.zip

    2020-04-23 17:36:14
    似玖玖视频影视源码1、首页对接苹果CMS直接采集影视资源【可以任意采集】2、首页UI修改更换【优化了细节】3、后台重新开发(修复漏洞越权问题)4、独立代{过}{滤}理后台5、支持投屏、分享、选集【注意本APP非全原生...
  • 【独家发布】互站购买的电影小程序源码/电影源码/视频源码/影视影院小程序/2020完整运营版本 ...东西还是不错的,带流量主激励广告,电视投屏,分享防封技术场景设置功能等,功能比较多。 东西如下图,有喜欢的拿去吧~
  • 苹果CMS蓝天APP源码

    2020-08-14 09:35:36
    苹果CMS蓝天APP源码是一个以uniapp...苹果CMS蓝天APP源码里面的视频数据明星数据会员等功能全是用的苹果CMSV10的数据。完美对接,另外开发了缓存,投屏,分享,兑换VIP。可以直接拿来运营。注意:安装本程序,需要先安
  • 苹果CMS原生萝卜影视源码APP源码,萝卜视频为全原生(并不是所谓的跨平台半原生请知晓)安卓:JAVA,后端用的是二次开发的苹果CMS,支持局域网投屏,视频软解硬解,播放器自带弹幕功能。支持解析官方视频,支持M3U8...
  • java安卓原生影视APP源码 对接苹果cms后台 支持投屏,选集,秒播,缓存下载等等,开发环境java安卓:AndroidAndroid Stodio对接苹果cms后台 原生并非h5 不管体验感觉还是使用h5都是无法对比的。天镶之别。原生播放器...
  • 源码优势:苹果CMS二次开发,支持局域网投屏,视频软解硬解,播放器自带弹幕功能。支持解析官方视频,支持M3U8,MP4。开屏广告,全局广告,自带模糊搜索功能(强大,打错字也能找到)。 解压密码:
  • 苹果CMS对接APP源码是一个以uniapp进行开发的苹果CMS的APP源码,本次更新较大,全局...功能点有,会员,三级分销,提现,充值,VIP,缓存,投屏,分享, 个人资料,明星,文章等等。。。基本苹果cms里面的功能都满足
  • 2021年最新修复的完整运营版本影视电影小程序源码 后端是苹果cms 内附搭建教程 带流量主激励广告,电视投屏,分享防封技术场景设置功能等,功能比较多。
  • Android Studio打包原生Java影视APP源码

    千次阅读 2020-10-09 09:08:29
    源码前端是用JAVA开发的全原生APP源码,后端用的是二次开发的苹果CMS,支持局域网投屏,视频软解硬解,播放器自带弹幕功能。支持解析官方视频,支持M3U8,MP4。 开屏广告,全局广告,APP数据均集成在苹果CMS后台设置...

    用Android Studio环境,搭建一个原生Java影视APP源码,各种功能也不错,就是有一点小bug

    Android Studio安装环境如果有人不会的,可能百度一下,看情况可能下次会出一个Android Studio安装教程

    源码前端是用JAVA开发的全原生APP源码,后端用的是二次开发的苹果CMS,支持局域网投屏,视频软解硬解,播放器自带弹幕功能。支持解析官方视频,支持M3U8,MP4。

    开屏广告,全局广告,APP数据均集成在苹果CMS后台设置,软件自带模糊搜索功能(强大,打错字也能找到)。

    源码包含:APP源码+二开苹果CMS源码+分享页源码+数据包+后端安装教程+前端安装打包详细教程
    源码我都是打包编译,安装测试过的。

    环境 Android Studio4.0 + PHP7.0
    各位可以自己测试下!

    测试APP:https://www.lanzoui.com/iyOCIh7wcoh

    链接:https://pan.baidu.com/s/18Vw-f5LLI9aNIbdyNSM-0g
    提取码:nl5s

    源码和教程已经打包在上面了,里面有详细教程,压缩包的解压密码是:wxys.vipyshy.com
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • 苹果CMS原生萝卜影视源码APP源码,萝卜视频为全原生(并不是所谓的跨平台半原生请知晓)安卓:JAVA,后端用的是二次开发的苹果CMS,支持局域网投屏,视频软解硬解,播放器自带弹幕功能。支持解析官方视频,支持M3U8...
  • 投屏功能 v2.4.7 报毒的问题 v2.4.6 设置回来了 密码锁 v2.4.5 首页变成了一行三列 增加了简易的iptv模块(视频源来自 ) 加了点广告 v2.4.4 “青少年模式”改变“关键字过滤” 解析接口兼容 选集支持单双列...
  • Shell的方式执行ADB相关命令,实现获取设备信息、投屏控制、全功能通用刷机、高速文件传输、微信/QQ文件提取备份、软件批量安装、预装应用卸载等功能 将复杂的操作简单化、自动化,使安卓高级操作上手更加容易 二. ...
  • 带流量主激励广告,电视投屏,分享防封技术场景设置功能等,功能比较多
  • 简介: 支持投屏,选集,秒播,缓存下载等等, ...原生播放器模块以及功能介绍:切换下一集,自动切换下一集,快退功能,双击暂停 ,选集,投屏。 网盘下载地址: http://kekewangLuo.net/kmJQRse0UPx0 图片: ...
  • 源码自带独家有效完美防黑功能 前端和后端 数据库都在里面了还有教程 只要有眼睛都可以搭建成功 稳定运营 安装说明: 后端替换域名app.stb789.cn 前端替换域名app.stb789.cn 软件名字:畅视影院 都替换 后台设置自己...
  • 介绍: 带流量主激励广告,电视投屏,分享防封技术场景设置功能等,功能比较多。 文件里有教程 网盘下载地址: https://zijiewangpan.com/0DfhtDJ8bDA 图片:
  • 是Genymobile公司出品的Android设备显示与操纵开源工具,已有投屏,控制,截图等功能。scrcpy-go是在其基础上制作的方便进行手机游戏的辅助工具,类似TC-Games出品的软件。 特别地,与scrcpy的主要不同点是: 使用...
  • Java版水果管理系统源码 lsp爬虫 LSP是第一生产力,深夜开车必备 你是不是也经常碰到无数的博(bo)彩(cai)广告、app下载跳出来,你是不是非常的懊恼,学习一下爬虫吧,只看你想看的内容 介绍 由“阿里渣渣java...
  • nfc感应开关全能投屏音乐接力碰触连网一碰连接米家智能场景 可参考项目 -能存网址的名片 KEY-存储密钥的带物理按键NFC -只是把NFCTag扔进两张纸之间去了。 乐器名片 带墨水屏的NFC标签 问题 手机的NFC功能可能会...
  • 带流量主激励广告,电视投屏,分享技术场景设置功能等,功能比较多。 搭建环境: 环境php7.0 — fileinfo–redis–sg11 mysql5.5 apache2.4 添加站点php7.0—-创建ftp—-上传后端文件《后端文件修改,/maccms/wxapi...
  • 原生安卓tv源码电视盒子app TVMovie JAVA源代码 APP开源全前端无需后台 AndroidTV、机顶盒、电影、电视剧、直播、远程搜索、远程网页投屏解析; 版本说明 1.2.4版本: 优化历史记录; 搜索改成单个数据源同步...
  •  后来吾向公司同事(吾亦是领导,就不强调领导了)展示了投屏功能。几个头目看了很有兴趣。吾突击了两个月,用C和安卓源码开发完成,然后集成到安卓版本里。C用的是安卓底层的一些接口,而安卓各家又不一样,自然...
  • 半年前做手机投屏功能,要求将同一局域网的设备列表传给前端进行交互,国庆前上线,然而就在测试前一天出问题反映情况是:iOS可以Android不行!Android的问题,加班给我改! 混合开发最最烦的事就是iOS,Android,...

空空如也

空空如也

1 2 3
收藏数 41
精华内容 16
关键字:

投屏功能源码