源碼地址:https://github.com/deepsadness/AppRemote
上一章中,我們簡(jiǎn)單實(shí)現(xiàn)了PC的投屏功能。 但是還是存在這一些缺陷。
屏幕的尺寸數(shù)據(jù)是寫死的 不能通過(guò)PC來(lái)對(duì)手機(jī)進(jìn)行控制 直接在主線程中進(jìn)行解碼和顯示,存在較大的延遲。
所以這邊文章。我們需要根據(jù)上面的需求。來(lái)對(duì)我們的代碼進(jìn)行優(yōu)化。
1. 屏幕信息發(fā)送
其實(shí)在上一章中,我們已經(jīng)獲取了屏幕信息。只是沒有發(fā)送給client端。這邊文章中,我們進(jìn)行發(fā)送。
android端 Android端在Socket連接成功后,就開啟發(fā)送
private static void sendScreenInfo(Size size, ByteBuffer buffer, FileDescriptor fileDescriptor) throws IOException {//將尺寸數(shù)據(jù)先發(fā)送過(guò)去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上負(fù)責(zé)接受,并設(shè)置給編碼器
//從客戶端接受屏幕數(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);
這樣就可以獲得屏幕的尺寸信息,保證不同手機(jī)分辨率也能正常使用了。
盡管我們通過(guò)這樣獲取了正確的屏幕信息,但是SDL顯示的畫面,還是有些奇怪。比我們預(yù)期的胖了一點(diǎn)。
通過(guò)下面的方式,來(lái)重新計(jì)算窗口的尺寸。這樣才能顯示正常。
//這里是給四周留空隙。
#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;}//設(shè)置大小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 0return current_size;}struct size display_size;// 32 bits because we need to multiply two 16 bits valuesint w;int h;if (!get_preferred_display_bounds(&display_size)) {// cannot get display bounds, do not constraint the sizew = 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);//縮放之后,保持長(zhǎng)寬比if (keep_width) {// remove black borders on top and bottomh = 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 bitsSDL_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)建windowsdl_window = SDL_CreateWindow(name,SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,window_size.width, window_size.height,SDL_WINDOW_RESIZABLE);
}
這樣才能顯示正常的窗口了。
?
正常的比例.png
2. 對(duì)Android手機(jī)進(jìn)行控制
我們知道在Android中有幾種方式可以對(duì)手機(jī)的Android發(fā)起模擬按鍵。
通過(guò)AccessibilityService的方式。通過(guò)注冊(cè)該服務(wù),可以捕獲所有的窗口變化,捕獲控鍵,進(jìn)行模擬點(diǎn)擊。 但是它需要額外的權(quán)限。 通過(guò)adb的方式 我們可以簡(jiǎn)單的通過(guò)adb shell input方法來(lái)完成模擬
Usage: input [<source>] <command> [<arg>...]The sources are: dpadkeyboardmousetouchpadgamepadtouchnavigationjoysticktouchscreenstylustrackballThe 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)
就可以對(duì)屏幕上(100,100)的位置,進(jìn)行模擬點(diǎn)擊。
通過(guò)InputManager實(shí)現(xiàn) 我們這里也是通過(guò)這個(gè)方式來(lái)實(shí)現(xiàn)的。
InputManager 模擬點(diǎn)擊事件
當(dāng)API 15之后,我們使用InputManager。
獲取InputManager 同樣可以通過(guò)Server Manager中就可以進(jìn)行獲取。
public InputManager getInputManager() {if (inputManager == null) {IInterface service = getService(Context.INPUT_SERVICE, "android.hardware.input.IInputManager");inputManager = new InputManager(service);}return inputManager;}
我們知道Android中的按鍵事件對(duì)應(yīng)的是KeyEvent,而手勢(shì)事件對(duì)應(yīng)的是MotionEvent。
public class KeyEventFactory {/*創(chuàng)建一個(gè)KeyEvent*/public static KeyEvent keyEvent(int action, int keyCode, int repeat, int metaState) {long now = SystemClock.uptimeMillis();/*** 1. 點(diǎn)擊的時(shí)間 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this key code originally went down.* 2. 事件發(fā)生的時(shí)間 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this event happened.* 3. UP DOWN MULTIPLE 中的一個(gè): either {@link #ACTION_DOWN},{@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.* 4. code The key code. 輸入的鍵盤事件* 5. 重復(fù)的事件次數(shù)。點(diǎn)出次數(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. 暫時(shí)不知道什么意思* 7. The device ID that generated the key event.* 8. Raw device scan code of the event. 暫時(shí)不知道什么意思* 9. The flags for this key event 暫時(shí)不知道什么意思* 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;}/*通過(guò)送入一個(gè)ACTION_DOWN 和ACTION_UP 來(lái)模擬一次點(diǎn)擊的事件*/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中的手勢(shì)事件的觸發(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);}
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);}}
值得注意的是:一次點(diǎn)擊事件是由一個(gè)DOWN 和UP事件組成的。
進(jìn)行通信
Client端(PC端)發(fā)送事件
通過(guò)SDL2的事件循環(huán)來(lái)監(jiān)聽,對(duì)輸入的事件進(jìn)行相應(yīng)
開啟事件循環(huán)
需要注意 的是:
必須在主線程內(nèi)(main方法所在的線程內(nèi))開啟事件循環(huán) 否則分分鐘給你一個(gè)異常。 開啟事件循環(huán)后,窗口上就出現(xiàn)按鈕了
? 開啟事件循環(huán)前
? 開啟事件循環(huán)后出現(xiàn)窗口上的按鈕.png
開啟事件循環(huán)代碼 :
//開啟Event Loopfor (;;) {SDL_WaitEvent(&event);//這里我們主要相應(yīng)了if (event.type == SDL_MOUSEBUTTONDOWN) { //點(diǎn)擊事件的DOWNhandleButtonEvent(sc, &event.button);} else if (event.type == SDL_MOUSEBUTTONUP) { //點(diǎn)擊事件的UPhandleButtonEvent(sc, &event.button);} else if (event.type == SDL_KEYDOWN) { //按鍵事件DOWNhandleSDLKeyEvent(sc, &event.key);} else if (event.type == SDL_KEYUP) { //按鍵事件UPhandleSDLKeyEvent(sc, &event.key);} else if (event.type == SDL_MOUSEWHEEL) { // 滾輪事件//處理滑動(dòng)事件handleScrollEvent(sc, &event.wheel);} else if (event.type == SDL_QUIT) { // 點(diǎn)擊窗口上的關(guān)閉按鈕printf("rev event type=SDL_QUIT\n");sc->destroy();break;}
事件處理代碼 : 其實(shí)就是將這些事件解析成坐標(biāo),然后通過(guò)socket發(fā)送
//對(duì)應(yīng)點(diǎn)擊事件
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;//是否超過(guò)來(lái)邊界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) {// ignorereturn;}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);
}// 對(duì)應(yīng)滑動(dòng)事件
// 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) {//處理滑動(dòng)事件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;//是否超過(guò)來(lái)邊界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) {// ignorereturn;}SDL_assert_release(x_c >= 0 && x_c < 0x10000 && y_c >= 0 && y_c < 0x10000);//使用這個(gè)來(lái)記錄滑動(dòng)的方向// SDL behavior seems inconsistent between horizontal and vertical scrolling// so reverse the horizontal// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>// SDL 的滑動(dòng)情況,兩個(gè)方向不一致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;//滾動(dòng)事件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ù)滾動(dòng)距離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);}//對(duì)應(yīng)鍵盤上的按鈕事件。
void handleSDLKeyEvent(SDL_Screen *sc, SDL_KeyboardEvent *event) {//分別對(duì)應(yīng) mac 上的 control option commandint 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);因?yàn)槲沂莔ac鍵盤,期望control+ H = home鍵 control+b = back鍵//再去取keycodeSDL_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) {//這個(gè)時(shí)候發(fā)送的是按下的狀態(tài)if (keycode == SDLK_h) {char buf[4];memset(buf, 0, sizeof(buf));buf[0] = 0;//自定義的案件事件buf[1] = 3;//1 是 downbuf[2] = 1;//key code home 鍵對(duì)應(yīng)的是 3buf[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 是 downbuf[2] = 1;//key code back 鍵對(duì)應(yīng)的是 4buf[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 是 upbuf[2] = 0;//key code home 鍵對(duì)應(yīng)的是 3buf[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 是 upbuf[2] = 0;//key code back 鍵對(duì)應(yīng)的是 4buf[3] = 4;int result = send(client_event, buf, 4, 0);printf("send result = %d\n", result);}}
}
這里可以看到,根據(jù)每一種事件,都定義了對(duì)應(yīng)的方式進(jìn)行發(fā)送。那Android端,可以通過(guò)對(duì)應(yīng)的方式進(jìn)行接收就可以了~
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());//當(dāng)讀到的長(zhǎng)度為0,就結(jié)束了。if (read == -1 || read == 0) {//如果這個(gè)時(shí)候read 0 的話。就結(jié)束break;} else {buffer.flip();//上面定義的,如果是按鈕事件,第一個(gè)必須是0byte b = buffer.get(0);//進(jìn)入對(duì)應(yīng)的事件if (b == 0 && read > 1) { //如果是0 的話,就當(dāng)作是Action//第2個(gè)是判斷事件的類型byte type = buffer.get(1);//按鍵事件。它發(fā)送時(shí)定義的長(zhǎng)度是6if (type < 2 && read == 6) {//action down 1 down 0 upSystem.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;//接受到事件進(jìn)行處理boolean key = createKey(serviceManager, type, x, y);buffer.clear();} else if (type == 2 && read == 14) { //滾動(dòng)事件.定義的長(zhǎng)度是14buffer.position(1);//x,y是接觸的點(diǎn),hs是水平的滑動(dòng),vs 是上下的滑動(dòng)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);//接受到事件進(jìn)行處理boolean b1 = injectScroll(serviceManager, x, y, hs, vs);// 處理完,記得清楚bufferbuffer.clear();} else if (type == 3 && read == 4) { //接受按鍵事件,長(zhǎng)度是4System.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);// 處理完,記得清楚bufferbuffer.clear();}}}} while (!eof);
這樣就可以進(jìn)行事件的相應(yīng)了。
顯示和處理事件的優(yōu)化
梳理優(yōu)化邏輯
解碼線程異步 雖然我們已經(jīng)通過(guò)Android的Api實(shí)現(xiàn)了按鍵注入,并且定義了Socket兩端對(duì)按鍵通信的協(xié)議。但是我們之前將解碼的循環(huán)已經(jīng)寫在主線程中了。這樣我們需要將事件的循環(huán)加入到主線程中,才能對(duì)事件發(fā)起響應(yīng)。 所以我們需要為我們的解碼循環(huán),創(chuàng)建一個(gè)解碼線程,在異步進(jìn)行解碼。 Socket通信異步 同時(shí),和上一章相同,結(jié)合我們豐富的開發(fā)經(jīng)驗(yàn)知道,我們不能將耗時(shí)任務(wù),放在主線程當(dāng)中。所以事件通信。我們也需要放到異步處理。 隊(duì)列操作 我們知道事件循環(huán)會(huì)源源不斷的送入,而我們的事件發(fā)送只能一個(gè)一個(gè)的發(fā)送。所以我們需要為事件循環(huán)加入隊(duì)列的緩存。從主線程中接受事件,從發(fā)送線程中,對(duì)隊(duì)列中的事件進(jìn)行一個(gè)一個(gè)的處理。 同時(shí),根據(jù)之前的學(xué)習(xí),我們也知道,我們的ffmpeg解碼和顯示其實(shí)也應(yīng)該加入隊(duì)列顯示。這樣我們就可以防止丟幀的存在。 但是我們這里為了簡(jiǎn)單顯示,只是緩存了兩幀。 一幀負(fù)責(zé)送顯。一幀負(fù)責(zé)接受解碼的幀。
線程模型
優(yōu)化后的線程模型如下:
- client端(PC)- event_loopSDL的EventLoop。復(fù)制渲染上屏和分發(fā)事件- event_sender(Socket send)接受SDL分發(fā)的事件。并把對(duì)應(yīng)的事件通過(guò)Socket分發(fā)給Android手機(jī)。- screen_receiver(Socket recv)通過(guò)Socket接受的 H264 Naul,使用FFmpeg進(jìn)行解碼。- server端(Android)- screen record (Socket InputStream)使用SurfaceControl和MediaCodec進(jìn)行屏幕錄制,錄制的結(jié)果通過(guò)Socket發(fā)送- event_loop (Socket OutputStream)接受Socket發(fā)送過(guò)來(lái)的事件。并調(diào)用對(duì)應(yīng)的API進(jìn)行事件的注入(InputManager)### 線程通信
- frames
兩塊緩存區(qū)域。- decode_frame解碼放置的frame- render_frame渲染需要的frame.使用該frame 進(jìn)行render
數(shù)據(jù)流動(dòng)- 生產(chǎn)的過(guò)程screen_receiver 負(fù)責(zé)生產(chǎn)。- 消費(fèi)的過(guò)程event_loop 負(fù)責(zé)消費(fèi)。將兩塊緩存區(qū)域進(jìn)行交換,并把render_frame上屏- event
一個(gè)event_queue隊(duì)列來(lái)接受。可以使用鏈表
數(shù)據(jù)流動(dòng)- 生產(chǎn)的過(guò)程event_loop 負(fù)責(zé)生產(chǎn)。并把數(shù)據(jù)送入隊(duì)列當(dāng)中- 消費(fèi)的過(guò)程event_sender 負(fù)責(zé)消費(fèi)。如果隊(duì)列不為空,則進(jìn)行發(fā)送
這里就不詳細(xì)說(shuō)明了。具體可以看代碼就明白了。
最后的結(jié)果
?
最后的結(jié)果.gif
就和Vysor和scrcpy一樣,我們可以通過(guò)投屏PC ,并操作手機(jī)了。而且在很低的延遲下。
?
源碼地址:https://github.com/deepsadness/AppProcessDemo
還有更多的細(xì)節(jié)處理,可以參考scrcpy
總結(jié)
Android PC投屏簡(jiǎn)單嘗試 這一系列文章,終于到了尾聲。總共橫跨了大半年的事件。 最后分成下面幾個(gè)方面來(lái)進(jìn)行一下總結(jié)
數(shù)據(jù)源
截屏數(shù)據(jù)的獲取
Android的MediaProjection API 通過(guò)MediaProjection的權(quán)限的獲取和調(diào)用其API就能創(chuàng)建一個(gè)屏幕的錄制屏幕 直接反射調(diào)用SurfaceControl的系列方法 因?yàn)樵赼pp_process下,我們有較高的權(quán)限。所以可以直接通過(guò)反射調(diào)用SurfaceControl 的方法,來(lái)完成錄制屏幕數(shù)據(jù)的獲取。(參考adb screenrecord 命令)
截屏數(shù)據(jù)的處理
MediaCodec硬件編碼 使用MediaCodec結(jié)合Surface ,能容易就能得到編碼后的H264數(shù)據(jù)。 使用ImageReader的方式。 使用ImageReader 的方式,可以獲取一幀一幀的數(shù)據(jù)。之后我們可以選擇直接發(fā)送Bitmap數(shù)據(jù)。或者結(jié)合自己的軟件解碼器(FFmpeg或者X264)來(lái)編碼獲得H264數(shù)據(jù)。
發(fā)送的協(xié)議
自己定義的Socket協(xié)議
就是適合簡(jiǎn)單的發(fā)送Bitmap。只要接受端能夠解析這個(gè)bitmap數(shù)據(jù),就可以完成數(shù)據(jù)的展示。
RTMP協(xié)議
可以通過(guò)在服務(wù)端建立RTMP協(xié)議,然后通過(guò)這個(gè)協(xié)議進(jìn)行。使用RTMP協(xié)議發(fā)送的好處在于,需要播放的端只要支持該協(xié)議,就可以輕松的進(jìn)行拉流播放。
通過(guò)USB和ADB協(xié)議進(jìn)行連接
這個(gè)僅僅適合于PC能夠直接用ADB和手機(jī)連接的場(chǎng)景。 但是在這個(gè)場(chǎng)景下,投屏的效果清晰,流暢,延遲很低。 暫時(shí)部分,因?yàn)橹苯影l(fā)送H264數(shù)據(jù),只要進(jìn)行解碼后,就可以進(jìn)行播放了。(文章使用了SDL2的方式進(jìn)行了方便的播放。)
知識(shí)點(diǎn)
整個(gè)過(guò)程中 我們對(duì)Media Codec和ImageReader/RTMP協(xié)議/FFmpeg/SDL2/Gradle進(jìn)行了知識(shí)點(diǎn)的串聯(lián)。 其實(shí)還是挺好玩的。
另外
如果是需要改成手機(jī)和手機(jī)連接。我們要怎么實(shí)現(xiàn)呢? 其實(shí)從上面不難看出。如果是手機(jī)和手機(jī)連接。 在近距離,我們可以簡(jiǎn)單的使用藍(lán)牙進(jìn)行Socket(類似ADB和USB的通信方式)。 如果是遠(yuǎn)距離,就可以通過(guò)RMTP的方式,來(lái)進(jìn)行推流和拉流。
最后,完結(jié)撒花?~~
投屏嘗試系列文章
Android PC投屏簡(jiǎn)單嘗試- 自定義協(xié)議章(Socket+Bitmap) Android PC投屏簡(jiǎn)單嘗試(錄屏直播)2—硬解章(MediaCodec+RMTP) Android PC投屏簡(jiǎn)單嘗試(錄屏直播)3—軟解章(ImageReader+FFMpeg with X264)
?
作者:deep_sadness 鏈接:https://www.jianshu.com/p/c2da5174d5f7 來(lái)源:簡(jiǎn)書 簡(jiǎn)書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
與50位技術(shù)專家面對(duì)面 20年技術(shù)見證,附贈(zèng)技術(shù)全景圖
總結(jié)
以上是生活随笔 為你收集整理的Android PC投屏简单尝试—最终章2 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。