Scrcpy源碼閱讀

1. 簡(jiǎn)介

開源項(xiàng)目:
https://github.com/Genymobile/scrcpy

項(xiàng)目簡(jiǎn)介:
通過在手機(jī)端使用虛擬顯示器進(jìn)行錄屏, 并直接使用手機(jī)自帶的視頻編碼器將屏幕數(shù)據(jù)編碼成視頻流(格式H264), 并將其發(fā)送成PC端, 使用ffmpeg對(duì)視頻流進(jìn)行解碼, 并通過SDL將手機(jī)屏幕鏡像顯示到電腦屏幕, 并且再通過控制流將PC端的鼠標(biāo)手勢(shì)等操作發(fā)送給APP端對(duì)手機(jī)進(jìn)行遠(yuǎn)程遙控.

技術(shù)點(diǎn):
該項(xiàng)目使用的技術(shù)和云游戲或手機(jī)直播使用的技術(shù)類似, 包括錄屏, 視頻流編碼, 推流, 視頻流解碼, 控制流遠(yuǎn)程操控等. 擴(kuò)展內(nèi)容需查看其他筆記: ffmpeg, WebRTC

目錄結(jié)構(gòu):

  • app:PC端,純C語言開發(fā), 基于ffmpeg和SDL開發(fā), 作為client端.
  • server,APP端,Java語言開發(fā), adb命令行下執(zhí)行的Java進(jìn)程(Dex格式)

2. APP端 (Java)

2.1 Server

Server.main()

  • createOptions()
    • maxSize // 最大尺寸
    • bitRate // 比特率
    • maxFps // 限幀
    • lockedVideoOrientation // 鎖定視頻方向
    • tunnelForward // 默認(rèn)false,app作為server,監(jiān)聽unix端口,adb forward到PC端口,等待PC端連接。true則反之,adb tunnel。
    • crop // 視頻截取尺寸
    • sendFrameMeta // 是否發(fā)送FrameMeta(和視頻流一起)
    • control // 是否控制
    • displayId // 屏幕ID
  • scrcpy(opts)
    • final Device device = new Device(options);
    • DesktopConnection connection = DesktopConnection.open(device, tunnelForward) // 建立與PC的連接, 詳見 2.2節(jié)
    • ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
    • if control:
      • Controller controller = new Controller(device, connection);
        • sender = new DeviceMessageSender(connection);
      • startController(controller); // 控制流
        • controller.control(); // 開線程調(diào)用, 詳見 2.4節(jié)
      • startDeviceMessageSender(controller.getSender());
        • sender.loop(); // 開線程調(diào)用
        • while true:
          • connection.sendDeviceMessage(clipboardTextEvent); // 發(fā)送剪貼板內(nèi)容給PC端
    • screenEncoder.streamScreen(device, connection.getVideoFd()); // 發(fā)送視頻流(阻塞), 詳見 2.3節(jié)

2.2 DesktopConnection

DesktopConnection.open(device, tunnelForward)

  • if tunnelForward:
    • LocalServerSocket localServerSocket = new LocalServerSocket("scrcpy"); // app作為server,監(jiān)聽在unix端口,adb forward到PC端口,等待PC端來連接
    • videoSocket = localServerSocket.accept(); // 視頻流
    • controlSocket = localServerSocket.accept(); // 控制流
  • else:
    • videoSocket = connect("scrcpy"); // app作為client,連接unix “scrcpy”端 <- adb reverse PC端口
    • controlSocket = connect("scrcpy");
  • DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
  • connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); // 像PC發(fā)送設(shè)備名稱,視頻長(zhǎng)寬尺寸, PC端讀取的代碼在device_read_info
    • buffer = new byte[64 + 4]
    • 64: deviceNames.getBytes()
    • 2: width
    • 2: height

DesktopConnection.receiveControlMessage()

  • msg = controlMessageReader.next()
  • controlMessageReader.readFrom(controlInputStream)
    • controlInputStream.read(rawBuffer, head, rawBuffer.length - head); // byte[] rawBuffer = new byte[1024];

2.3 ScreenEncoder

ScreenEncoder.streamScreen(device, videoFd)

  • Looper.prepareMainLooper();
  • Workarounds.fillAppInfo();
    • new android.app.ActivityThread()
    • Application app = Instrumentation.newApplication(Application.class, ctx);
    • .....
  • createFormat()
    • MediaFormat format = new MediaFormat();
    • format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
    • format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
  • MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); // 創(chuàng)建視頻編碼器
  • IBinder display = SurfaceControl.createDisplay("scrcpy", true); // 創(chuàng)建虛擬屏幕 , 詳見 2.5小節(jié)
  • setSize()
    • format.setInteger(MediaFormat.KEY_WIDTH, width);
    • format.setInteger(MediaFormat.KEY_HEIGHT, height);
  • codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  • Surface surface = codec.createInputSurface();
  • setDisplaySurface()
    • SurfaceControl.openTransaction();
    • SurfaceControl.setDisplaySurface(display, surface);
    • SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
    • SurfaceControl.setDisplayLayerStack(display, layerStack);
    • SurfaceControl.closeTransaction();
  • codec.start();
  • alive = encode(codec, fd);
    • int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); // 獲取輸出的編碼bufferID
    • ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); // 獲取編碼后的視頻buffer
    • if sendFrameMeta: // 總是true
      • writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); // 發(fā)送FrameMeta
    • IO.writeFully(fd, codecBuffer); // 發(fā)送buffer到視頻流
  • codec.stop();

2.4 Controller

Controller.control()

  • if not device.isScreenOn(): // 如果屏幕沒亮,則點(diǎn)擊電源鍵點(diǎn)亮屏幕
    • injectKeyCode(KeyEvent.KEYCODE_POWER)
  • while true:
    • handleEvent();
      • msg = connection.receiveControlMessage()
      • switch msg.type:
        • case TYPE_INJECT_KEYCODE:
        • case TYPE_INJECT_TEXT
        • case TYPE_INJECT_TOUCH_EVENT
        • case TYPE_INJECT_SCROLL_EVENT
        • case TYPE_BACK_OR_SCREEN_ON
        • case TYPE_EXPAND_NOTIFICATION_PANEL
        • case TYPE_COLLAPSE_NOTIFICATION_PANEL
        • case TYPE_GET_CLIPBOARD
          • sender.pushClipboardText(serviceManager.getClipboardManager().getText());
        • case TYPE_SET_CLIPBOARD
          • device.setClipboardText(msg.getText());
            • serviceManager.getClipboardManager().setText(text);
        • case TYPE_SET_SCREEN_POWER_MODE
          • device.setScreenPowerMode(msg.getAction());
            • SurfaceControl.setDisplayPowerMode()
        • case TYPE_ROTATE_DEVICE

2.5 SurfaceControl

SurfaceControl

  • createDisplay() // 創(chuàng)建虛擬顯示器
    • android.view.SurfaceControl.createDisplay(name, secure)
  • setDisplaySurface()
    • android.view.SurfaceControl.setDisplaySurface(display, surface)
  • setDisplayProjection()
    • android.view.SurfaceControl.setDisplayProjection(display, orientaion, layerStackRect, displayRect)
  • setDisplayLayerStack()
    • android.view.SurfaceControl.setDisplayLayerStack(display, layerStack)
  • openTransaction()
    • android.view.SurfaceControl.openTransaction()
  • closeTransaction()
    • android.view.SurfaceControl.closeTransaction()
  • getBuiltInDisplay()
    • android.view.SurfaceControl.getBuiltInDisplay() // or getInternalDisplayToken() if sdk >= android Q
  • setDisplayPowerMode()
    • android.view.SurfaceControl.setDisplayPowerMode()
  • destroyDisplay()
    • android.view.SurfaceControl.destroyDisplay()

為什么用反射去調(diào)用 android.view.SurfaceControl 接口,而不是使用如下的接口:

import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;


virtualDisplay = mediaProjection. **createVirtualDisplay** ("WebRTC_ScreenCapture", width, height,
VIRTUAL_DISPLAY_DPI, DISPLAY_FLAGS, new Surface(surfaceTextureHelper.getSurfaceTexture()),
null /* callback */, null /* callback handler */);


這些接口可能需要權(quán)限,以及Context,而在命令行運(yùn)行的dex沒有這些。

3. PC端 (C語言)

3.1 main

main()

  • scrcpy_parse_args() // 解析參數(shù), TODO
    • serial // 多臺(tái)adb device時(shí)指定需要連接的serial
  • av_register_all() // ffmpeg注冊(cè)所有視頻編碼格式
  • avformat_network_init() // ffmpeg初始化網(wǎng)絡(luò)格式, TODO
  • scrcpy(args.opts)
    • server_start() // 開啟本地服務(wù)

      • push_server() // 將APP端的server文件推送(adb push)到手機(jī)
      • enable_tunnel_any_port()
        • enable_tunnel_reverse_any_port() // PC端作為server,監(jiān)聽在local_port, 等待APP端來連接
          • adb reverse tcp:<local_port> localabstract:scrcpy
          • server_socket = listen_on_port(port)
            • net_connect("localhost", port)
              • sock = socket(AF_INET, SOCK_STREAM, 0)
              • setsockopt(sock, ...)
              • bind(sock)
              • listen(sock)
        • or enable_tunnel_forward_any_port() // PC端作為client,通過adb forward去連接監(jiān)聽在scrcpy端口的APP端的server
          • adb forward tcp:<local_port> localabstract:scrcpy
      • server->process = execute_server(server, params) // 拉起PC端的server
        • adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process
      • SDL_CreateThread(run_wait_server)
        • cmd_simple_wait(server->process)
    • sdl_init_and_configure(display, render_driver) // SDL初始化

      • SDL_Init()
      • SDL_SetHint(SDL_HINT_RENER_DRIVER,options->render_driver) // "direct3d", "opengl", "opengles2", "opengles", "metal" and "software"
      • SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")
      • SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")
      • SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")
      • SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")
      • SDL_EnableScreenSaver()
    • server_connect_to() // 連接APP端

      • if tunnel_forward:
        • erver->video_socket = net_accept(server->server_socket);
        • server->control_socket = net_accept(server->server_socket);
      • else:
        • server->video_socket = connect_to_server(server->local_port, attempts, delay);
        • server->control_socket = net_connect(IPV4_LOCALHOST, server->local_port);
    • device_read_info(server.video_socket) // 讀取手機(jī)設(shè)備名, 對(duì)應(yīng) 2.2小節(jié)

    • fps_counter_init() // ALT+I 在控制臺(tái)顯示FPS

    • video_buffer_init(video_buffer) // 初始化視頻buffer

    • if control:

      • file_handler_init() // 初始化控制流
    • decoder_init() // 初始化解碼器

    • recorder_init() // 初始化錄制器(直接錄制視頻到文件)

    • av_log_set_callback(av_log_callback); // ffmpeg日志回調(diào), TODO

    • stream_init() // 初始化視頻流

    • stream_start()

    • controller_init() // 初始化控制流流

    • controller_start()

    • screen_init_rendering() // 詳見 screen.c, 初始化渲染

    • if opts.turn_screen_off()

      • controller_push_msg(screen_power_mode_off_msg) // 關(guān)閉手機(jī)屏幕
    • if opts.fullscreen():

      • screen_switch_fullscreen() // 切換PC端全屏
    • if opts.show_touches:

      • wait_show_touches() // 顯示觸摸
    • event_loop()

      • while SDL_WaitEvent(&event): // SDL事件驅(qū)動(dòng)主循環(huán)
        • handle_event(&event, control)
          • switch event.type:
            • case EVENT_NEW_FRAME
            • case SDL_WINDOWEVENT
            • case SDL_TEXTINPUT
            • case SDL_KEYDOWN
            • case SDL_KEYUP
            • case SDL_MOUSEMOTION
            • case SDL_MOUSEWHEEL
            • case SDL_MOUSEBUTTONDOWN
            • case SDL_MOUSEBUTTONUP
            • case SDL_FINGERMOTION
            • case SDL_FINGERDOWN
            • case SDL_FINGERUP
            • case SDL_DROPFILE
    • screen_destroy()

  • avformat_network_deinit()

execute_server: 拉起PC端的命令及參數(shù)說明:

adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process
/                                   // unused
com.genymobile.scrcpy.Server        // java main class
1.13                                // version
0                                   // max_size
8000000                             // bit_rate
0                                   // max_fps
-1                                  // lock_video_orientation
false                               // trunel_forward
-                                   // crop
true                                // send frame meta
true                                // iscontrol

NOTE ATTRIBUTES

Created Date: 2020-05-18 04:41:29
Last Evernote Update Date: 2020-05-20 03:22:46

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容