레이블이 scrcpy인 게시물을 표시합니다. 모든 게시물 표시
레이블이 scrcpy인 게시물을 표시합니다. 모든 게시물 표시

2021년 9월 22일 수요일

scrcpy (안드로이드 단말을 PC에 연결해서 화면 보기, 스크린 미러링) #4 소스 분석


앞에서 jar파일의 main이 sever.java여기에 있고 실행시키는것까지 확인하였습니다.

server.java에서는 아마도(?) 소켓을 만들고 대기하고 있을것입니다.

전형적인 PME(Property Method Event)프로그래밍 방식에서는 무한 루프에 빠지는 형태를 취하지는 않습니다. 그러나 여기 소스는 android의 cmd line 형태를 취하고 있고 전혀 android activity 구조를 가지고 있지 않습니다.


scrcpy 함수에서 아래 함수를 볼 수 있는데

screenEncoder.streamScreen(device, connection.getVideoFd());

이 부분이 화면을 압축하여 지속적으로 전달하기 위한 함수 입니다.

내부에는 주요 함수들도 많지만, 특히 아래함수가 주요 함수로 보면됩니다.

private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {





다음으로는 입력에대한 처리입니다.

scrcpy 소스 근처의 controller 쪽을 참고 하면됩니다.

                final Controller controller = new Controller(device, connection);


                // asynchronous

                controllerThread = startController(controller);

thread를 만들고 control 함수를 실행시키게 되는데 이부분의 소스는 Controller.java가 담당합니다.

public void control() throws IOException {
// on start, power on the device
if (!Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
while (true) {
handleEvent();
}
}

별건 없고 hadleEvenet게 주업무를 담당하고 있습니다.

키를 누르거나 터치를 하게 되면 아래 함수에 의해서 처리가 되는데


private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:


누군가가 메세지를 보내준걸 읽게 됩니다.

public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
}
return msg;
}

그렇다면 그 누군가가 누구일까요? 바로 PC의 connection온 데이터 겠죠. 모든 입력은 PC쪽에서 발생하게 되고 server jar 쪽으로 전달되게 됩니다.

아래와 같은 함수로 touch가 눌린것처럼 행동하게 됩니다.

injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());

좀 더 따라가보면

아래 형태로 inject 를 하게 됩니다.

public static boolean injectEvent(InputEvent inputEvent, int displayId) {
if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

여기에서 inputmanager는 이렇고,

public InputManager getInputManager() {
if (inputManager == null) {
inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
}
return inputManager;
}

여기에 전달하는 inputevent는 아래와 같습니다

Controller.java 소스를 참고 하면 됩니다.

MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);
return device.injectEvent(event);







2021년 9월 12일 일요일

scrcpy (안드로이드 단말을 PC에 연결해서 화면 보기, 스크린 미러링) #3 소스 분석

 

jar파일을 android에서 실행시킬 수 있을까?

구글링을 해보면 할 수 없다는 내용이 대부분입니다. 

여기에서는 어떤 식으로 jar 파일을 생성 했는지 알아보도록 하겠습니다.


안드로이드의 기본이라면 manifest 부터 봐야겠습니다.

https://github.com/Genymobile/scrcpy/blob/master/server/src/main/AndroidManifest.xml

원래는 여기에 activity 정보가 기록되야 하지만 패키지 이름만 있을뿐 특이점이라면 아무것도 없습니다.


결국 adb에서 jar 파일을 보내고 나서 실행하는 부분을 다시 살펴보자면, 아래 소스에서 찾아볼 수 있습니다.

https://github.com/Genymobile/scrcpy/blob/master/app/src/server.c

static process_t
execute_server(struct server *server, const struct server_params *params) {
    char max_size_string[6];
    char bit_rate_string[11];
    char max_fps_string[6];
    char lock_video_orientation_string[5];
    char display_id_string[11];
    sprintf(max_size_string, "%"PRIu16, params->max_size);
    sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
    sprintf(max_fps_string, "%"PRIu16, params->max_fps);
    sprintf(lock_video_orientation_string, "%"PRIi8,
            params->lock_video_orientation);
    sprintf(display_id_string, "%"PRIu32, params->display_id);
    const char *const cmd[] = {
        "shell",
        "CLASSPATH=" DEVICE_SERVER_PATH,
        "app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
# ifdef SERVER_DEBUGGER_METHOD_NEW
        /* Android 9 and above */
        "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
        "server=y,address="
# else
        /* Android 8 and below */
        "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
            SERVER_DEBUGGER_PORT,
#endif
        "/", // unused
        "com.genymobile.scrcpy.Server",
        SCRCPY_VERSION,
        log_level_to_server_string(params->log_level),
        max_size_string,
        bit_rate_string,
        max_fps_string,
        lock_video_orientation_string,
        server->tunnel_forward ? "true" : "false",
        params->crop ? params->crop : "-",
        "true", // always send frame meta (packet boundaries + timestamp)
        params->control ? "true" : "false",
        display_id_string,
        params->show_touches ? "true" : "false",
        params->stay_awake ? "true" : "false",
        params->codec_options ? params->codec_options : "-",
        params->encoder_name ? params->encoder_name : "-",
        params->power_off_on_close ? "true" : "false",
    };
#ifdef SERVER_DEBUGGER
    LOGI("Server debugger waiting for a client on device port "
         SERVER_DEBUGGER_PORT "...");
    // From the computer, run
    //     adb forward tcp:5005 tcp:5005
    // Then, from Android Studio: Run > Debug > Edit configurations...
    // On the left, click on '+', "Remote", with:
    //     Host: localhost
    //     Port: 5005
    // Then click on "Debug"
#endif
    return adb_execute(server->serial, cmd, ARRAY_LEN(cmd));
}


cmd 내용을 보자면 아래과 같은 형태로 실행됩니다.

adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process -XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y, server=y,address= / com.genymobile.scrcpy.Server ... 대충 이런형태가 됩니다.


app_process에 대한 정보는 여기도 참고 바랍니다.

https://codechacha.com/ko/convert-jar-to-dex-and-run-on-device/


핵심은 app_process를 실행키시면서 옵션을 전달하는 형태가 되며, app_process란 process를 실행시키기 위한 zygote(수정란) 으로 생각하면 됩니다.


jar 파일은 여기에 있는 소스들을 빌드해서 만들어지며

https://github.com/Genymobile/scrcpy/tree/master/server/src/main/java/com/genymobile/scrcpy

java의 특성상 실행은 static main 함수를 찾아서 실행하게 됩니다.

    public static void main(String... args) throws Exception {

해당 소스는 server.java파일 내에 있습니다.


2021년 9월 5일 일요일

scrcpy (안드로이드 단말을 PC에 연결해서 화면 보기, 스크린 미러링) #2 소스 분석


계속해서 scrcpy의 소스 구조를 탐험하였다.

지난번에 확인한것은 adb 명령으로 jar파일을 넣고 해당 jar파일을 실행시키는것까지 확인하였다.

다음으로 궁금한것은 화면을 어떤 방식으로 전달하고 있는가이다.


pc 용 프로그램에서는

scrcpy() 함수를 호출하고, 해당 함수가 끝나면 종료되는 구조


이전 글에서 설명한 부분이 server_start()함수이며, jar 파일 전송과 실행을 adb로 담당하게되고

    if (!server_start(&s->server, &params)) {

        goto end;

    }

SDL은 PC에서 화면을 출력하기 위한 라이브러리이다.

    if (!sdl_init_and_configure(options->display, options->render_driver,

                                options->disable_screensaver)) {

        goto end;

    }

아래 부분이 이번에 다루게될 부분으로 통신이 어떻게 된건지 해부해볼 부분이다.

    if (!server_connect_to(&s->server, device_name, &frame_size)) {

        goto end;

    }

함수에서 server->video_socket 부분을 따라가면 되는데, 그 외에도 제어를 위해서 server->control_socket 도 있긴합니다.

일단 video_socket 부분만 확인하면 아래와 같은 코드들이 보입니다.

server->video_socket = net_accept(server->server_socket);

server->video_socket =

            connect_to_server(server->local_port, attempts, delay);


enable_tunnel_any_port


기본적인 통신은 소켓을 이용하여 통신을 하게 되는데, 

PC쪽에 서버 소켓을 만들지 , android 쪽에 만들지에 대한 부분이 차이가 나서 코드가 복잡하게 되어있다.


이 내용은 안드로이 단말에 올라가는 jar 파일 소스를 보면 좀 더 명확해진다.

https://github.com/Genymobile/scrcpy/blob/master/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java


    public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
        LocalSocket videoSocket;
        LocalSocket controlSocket;
        if (tunnelForward) {
            LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
            try {
                videoSocket = localServerSocket.accept();
                // send one byte so the client may read() to detect a connection error
                videoSocket.getOutputStream().write(0);
                try {
                    controlSocket = localServerSocket.accept();
                } catch (IOException | RuntimeException e) {
                    videoSocket.close();
                    throw e;
                }
            } finally {
                localServerSocket.close();
            }
        } else {
            videoSocket = connect(SOCKET_NAME);
            try {
                controlSocket = connect(SOCKET_NAME);
            } catch (IOException | RuntimeException e) {
                videoSocket.close();
                throw e;
            }
        }

        DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
        Size videoSize = device.getScreenInfo().getVideoSize();
        connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
        return connection;
    }


코드는 이부분이고 tunnelForward 에 따라 자체적으로 localserver socket 으로 만들지 아니면 접속할지 코드가 나뉘어져 있다.


오늘은 통신이 연결된 구조에 대해 알아보았습니다.

한줄 요약 : 로컬 소켓 통신(TCP/IP)을 하도록 구성되어있음



2021년 8월 29일 일요일

scrcpy (안드로이드 단말을 PC에 연결해서 화면 보기, 스크린 미러링)

 

https://github.com/Genymobile/scrcpy


이 어플리케이션은 USB ( 혹은 TCP/IP ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다.


실행파일은 git hub에 컴파일된 링크가 있습니다.

Get the app

Summary

  • Linux: apt install scrcpy
  • Windows: download
  • macOS: brew install scrcpy

Build from sources: BUILD (simplified process)


windows용을 받으면

adb와 dll 몇개와 배치파일로 구성되어져 있습니다.

scrcpy-console.bat 파일을 실행키면 동작이 됩니다. 단말은 adb가 가능한 상태가 되어있어야 합니다.


구성은 scrcpy-server 와 scrcpy.exe 두개가 주된 기능을 가지게 됩니다.



소스 분석

빌드하는쪽을 따라가면 소스가 어떤형태로 되어있는지 분석이 가능합니다.

scrcpy-server

https://github.com/Genymobile/scrcpy/blob/master/release.mk

SERVER_BUILD_DIR := build-server

서버 소스는 여기에 https://github.com/Genymobile/scrcpy/tree/master/server

https://github.com/Genymobile/scrcpy/blob/master/server/src/main/java/com/genymobile/scrcpy/Server.java

    public static void main(String... args) throws Exception {

        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

            @Override

            public void uncaughtException(Thread t, Throwable e) {

                Ln.e("Exception on thread " + t, e);

                suggestFix(e);

            }

        });

        Options options = createOptions(args);

        Ln.initLogLevel(options.getLogLevel());

        scrcpy(options);

    }



scrcpy.exe

https://github.com/Genymobile/scrcpy/tree/master/app

main.c->scrcpy.c

adb로 jar파일을 push한뒤 실행시킴

https://github.com/Genymobile/scrcpy/blob/master/app/src/server.c

if (!push_server(params->serial)) {

#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"

server->process = execute_server(server, params);

const char *const cmd[] = {
"shell",
"CLASSPATH=" DEVICE_SERVER_PATH,
"app_process",

return adb_execute(server->serial, cmd, ARRAY_LEN(cmd));