源碼地址:https://github.com/deepsadness/AppRemote
上一章中,我們簡單實現(xiàn)了PC的投屏功能。
但是還是存在這一些缺陷。
- 屏幕的尺寸數(shù)據(jù)是寫死的
- 不能通過PC來對手機進行控制
- 直接在主線程中進行解碼和顯示,存在較大的延遲。
所以這邊文章。我們需要根據(jù)上面的需求。來對我們的代碼進行優(yōu)化。
1. 屏幕信息發(fā)送
其實在上一章中,我們已經(jīng)獲取了屏幕信息。只是沒有發(fā)送給client端。這邊文章中,我們進行發(fā)送。
- android端
Android端在Socket連接成功后,就開啟發(fā)送
private static void sendScreenInfo(Size size, ByteBuffer buffer, FileDescriptor fileDescriptor) throws IOException {
//將尺寸數(shù)據(jù)先發(fā)送過去
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("發(fā)送尺寸 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("發(fā)送尺寸 size result ");
buffer.clear();
}
- Client端
在PC上負責接受,并設置給編碼器
//從客戶端接受屏幕數(shù)據(jù)
uint8_t size[4];
socketConnection->recv_from_(reinterpret_cast<uint8_t *>(size), 4);
//這里先寫死,后面從客戶端內(nèi)接受
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};
}
//調(diào)用
void set(){
struct size frame_size = {
.height=screen_h,
.width=screen_w
};
struct size window_size = get_optimal_size(frame_size, frame_size);
//創(chuàng)建window
sdl_window = SDL_CreateWindow(
name,
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height,
SDL_WINDOW_RESIZABLE);
}
這樣才能顯示正常的窗口了。

2. 對Android手機進行控制
我們知道在Android中有幾種方式可以對手機的Android發(fā)起模擬按鍵。
- 通過AccessibilityService的方式。通過注冊該服務,可以捕獲所有的窗口變化,捕獲控鍵,進行模擬點擊。
但是它需要額外的權限。 - 通過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)的位置,進行模擬點擊。
- 通過InputManager實現(xiàn)
我們這里也是通過這個方式來實現(xiàn)的。
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。
- 創(chuàng)建KeyEvent
public class KeyEventFactory {
/*
創(chuàng)建一個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. 事件發(fā)生的時間 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. 重復的事件次數(shù)。點出次數(shù)? 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)};
}
}
- 創(chuàng)建MotionEvent
Android中的手勢事件的觸發(fā)。
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
最后是調(diào)用注入該事件
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端)發(fā)送事件
通過SDL2的事件循環(huán)來監(jiān)聽,對輸入的事件進行相應
開啟事件循環(huán)
需要注意的是:
- 必須在主線程內(nèi)(main方法所在的線程內(nèi))開啟事件循環(huán)
否則分分鐘給你一個異常。 -
開啟事件循環(huán)后,窗口上就出現(xiàn)按鈕了
開啟事件循環(huán)前
開啟事件循環(huán)后出現(xiàn)窗口上的按鈕.png
開啟事件循環(huán)代碼 :
//開啟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發(fā)送
//對應點擊事件
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) {
//發(fā)送down 事件
buf[1] = 1;
} else {
// 發(fā)送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;
//繼續(xù)滾動距離
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) {
//這個時候發(fā)送的是按下的狀態(tài)
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);
}
}
}
這里可以看到,根據(jù)每一種事件,都定義了對應的方式進行發(fā)送。那Android端,可以通過對應的方式進行接收就可以了~
- Server端(Android端)接收事件
接收client端發(fā)送的事件。將其解析,注入
do {
//讀到數(shù)據(jù)
int read = Os.read(fileDescriptor, buffer);
System.out.println("read=" + read + ",position=" + buffer.position() + "," +
"limit=" + buffer.limit() + ",remaining " + buffer.remaining());
//當讀到的長度為0,就結(jié)束了。
if (read == -1 || read == 0) {
//如果這個時候read 0 的話。就結(jié)束
break;
} else {
buffer.flip();
//上面定義的,如果是按鈕事件,第一個必須是0
byte b = buffer.get(0);
//進入對應的事件
if (b == 0 && read > 1) { //如果是0 的話,就當作是Action
//第2個是判斷事件的類型
byte type = buffer.get(1);
//按鍵事件。它發(fā)送時定義的長度是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);
這樣就可以進行事件的相應了。
顯示和處理事件的優(yōu)化
梳理優(yōu)化邏輯
- 解碼線程異步
雖然我們已經(jīng)通過Android的Api實現(xiàn)了按鍵注入,并且定義了Socket兩端對按鍵通信的協(xié)議。但是我們之前將解碼的循環(huán)已經(jīng)寫在主線程中了。這樣我們需要將事件的循環(huán)加入到主線程中,才能對事件發(fā)起響應。
所以我們需要為我們的解碼循環(huán),創(chuàng)建一個解碼線程,在異步進行解碼。 - Socket通信異步
同時,和上一章相同,結(jié)合我們豐富的開發(fā)經(jīng)驗知道,我們不能將耗時任務,放在主線程當中。所以事件通信。我們也需要放到異步處理。 - 隊列操作
我們知道事件循環(huán)會源源不斷的送入,而我們的事件發(fā)送只能一個一個的發(fā)送。所以我們需要為事件循環(huán)加入隊列的緩存。從主線程中接受事件,從發(fā)送線程中,對隊列中的事件進行一個一個的處理。
同時,根據(jù)之前的學習,我們也知道,我們的ffmpeg解碼和顯示其實也應該加入隊列顯示。這樣我們就可以防止丟幀的存在。
但是我們這里為了簡單顯示,只是緩存了兩幀。
一幀負責送顯。一幀負責接受解碼的幀。
線程模型
優(yōu)化后的線程模型如下:
- client端(PC)
- event_loop
SDL的EventLoop。復制渲染上屏和分發(fā)事件
- event_sender(Socket send)
接受SDL分發(fā)的事件。并把對應的事件通過Socket分發(fā)給Android手機。
- screen_receiver(Socket recv)
通過Socket接受的 H264 Naul,使用FFmpeg進行解碼。
- server端(Android)
- screen record (Socket InputStream)
使用SurfaceControl和MediaCodec進行屏幕錄制,錄制的結(jié)果通過Socket發(fā)送
- event_loop (Socket OutputStream)
接受Socket發(fā)送過來的事件。并調(diào)用對應的API進行事件的注入(InputManager)
### 線程通信
- frames
兩塊緩存區(qū)域。
- decode_frame
解碼放置的frame
- render_frame
渲染需要的frame.使用該frame 進行render
數(shù)據(jù)流動
- 生產(chǎn)的過程
screen_receiver 負責生產(chǎn)。
- 消費的過程
event_loop 負責消費。將兩塊緩存區(qū)域進行交換,并把render_frame上屏
- event
一個event_queue隊列來接受??梢允褂面湵?數(shù)據(jù)流動
- 生產(chǎn)的過程
event_loop 負責生產(chǎn)。并把數(shù)據(jù)送入隊列當中
- 消費的過程
event_sender 負責消費。如果隊列不為空,則進行發(fā)送
這里就不詳細說明了。具體可以看代碼就明白了。
最后的結(jié)果

就和
Vysor和scrcpy一樣,我們可以通過投屏PC ,并操作手機了。而且在很低的延遲下。
源碼地址:https://github.com/deepsadness/AppProcessDemo
還有更多的細節(jié)處理,可以參考scrcpy
總結(jié)
Android PC投屏簡單嘗試 這一系列文章,終于到了尾聲。總共橫跨了大半年的事件。
最后分成下面幾個方面來進行一下總結(jié)
數(shù)據(jù)源
截屏數(shù)據(jù)的獲取
- Android的MediaProjection API
通過MediaProjection的權限的獲取和調(diào)用其API就能創(chuàng)建一個屏幕的錄制屏幕 - 直接反射調(diào)用SurfaceControl的系列方法
因為在app_process下,我們有較高的權限。所以可以直接通過反射調(diào)用SurfaceControl
的方法,來完成錄制屏幕數(shù)據(jù)的獲取。(參考adb screenrecord 命令)
截屏數(shù)據(jù)的處理
- MediaCodec硬件編碼
使用MediaCodec結(jié)合Surface ,能容易就能得到編碼后的H264數(shù)據(jù)。 - 使用ImageReader的方式。
使用ImageReader 的方式,可以獲取一幀一幀的數(shù)據(jù)。之后我們可以選擇直接發(fā)送Bitmap數(shù)據(jù)。或者結(jié)合自己的軟件解碼器(FFmpeg或者X264)來編碼獲得H264數(shù)據(jù)。
發(fā)送的協(xié)議
自己定義的Socket協(xié)議
就是適合簡單的發(fā)送Bitmap。只要接受端能夠解析這個bitmap數(shù)據(jù),就可以完成數(shù)據(jù)的展示。
RTMP協(xié)議
可以通過在服務端建立RTMP協(xié)議,然后通過這個協(xié)議進行。使用RTMP協(xié)議發(fā)送的好處在于,需要播放的端只要支持該協(xié)議,就可以輕松的進行拉流播放。
通過USB和ADB協(xié)議進行連接
這個僅僅適合于PC能夠直接用ADB和手機連接的場景。
但是在這個場景下,投屏的效果清晰,流暢,延遲很低。
暫時部分,因為直接發(fā)送H264數(shù)據(jù),只要進行解碼后,就可以進行播放了。(文章使用了SDL2的方式進行了方便的播放。)
知識點
整個過程中
我們對Media Codec和ImageReader/RTMP協(xié)議/FFmpeg/SDL2/Gradle進行了知識點的串聯(lián)。
其實還是挺好玩的。
另外
如果是需要改成手機和手機連接。我們要怎么實現(xiàn)呢?
其實從上面不難看出。如果是手機和手機連接。
在近距離,我們可以簡單的使用藍牙進行Socket(類似ADB和USB的通信方式)。
如果是遠距離,就可以通過RMTP的方式,來進行推流和拉流。
最后,完結(jié)撒花??~~


