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端
- Controller controller = new Controller(device, connection);
- 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:
- if device.supportsInputEvents():
- injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
- KeyEvent event = new KeyEvent();
- device.injectInputEvent(event)
- injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
- if device.supportsInputEvents():
- 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);
- device.setClipboardText(msg.getText());
- case TYPE_SET_SCREEN_POWER_MODE
- device.setScreenPowerMode(msg.getAction());
- SurfaceControl.setDisplayPowerMode()
- device.setScreenPowerMode(msg.getAction());
- case TYPE_ROTATE_DEVICE
- case TYPE_INJECT_KEYCODE:
- handleEvent();
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)
- net_connect("localhost", port)
- or enable_tunnel_forward_any_port() // PC端作為client,通過adb forward去連接監(jiān)聽在scrcpy端口的APP端的server
- adb forward tcp:<local_port> localabstract:scrcpy
- enable_tunnel_reverse_any_port() // PC端作為server,監(jiān)聽在local_port, 等待APP端來連接
- 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);
- if tunnel_forward:
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
- switch event.type:
- handle_event(&event, control)
- while SDL_WaitEvent(&event): // SDL事件驅(qū)動(dòng)主循環(huán)
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