scrcpy服务端代码分析
Posted li_Jiejun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scrcpy服务端代码分析相关的知识,希望对你有一定的参考价值。
源码
Git clone https://github.com/barry-ran/QtScrcpy.git
Server端
入口文件 src/main/java/com/genymobile/scrcpy/Server.java
入口main函数
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); //解析参数配置,总共有14个参数
Ln.initLogLevel(options.getLogLevel());
scrcpy(options); //下一步
下一步scrcpy函数
private static void scrcpy(Options options) throws IOException
……
final Device device = new Device(options);
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) //创建socket连接
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); //屏幕编码
if (options.getControl())
final Controller controller = new Controller(device, connection);
// asynchronous
startController(controller); //指令流监听并做处理,使用的线程,内部死循环监听处理
startDeviceMessageSender(controller.getSender()); //监听剪切板事件
device.setClipboardListener(new Device.ClipboardListener()
@Override
public void onClipboardTextChanged(String text)
controller.getSender().pushClipboardText(text);
);
try
// synchronous
screenEncoder.streamScreen(device, connection.getVideoFd()); //视频流编码
catch (IOException e)
// this is expected on close
Ln.d("Screen streaming stopped");
创建localsocket,视频流和指令流同属一个socket
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(); //创建socket并处于阻塞状态
// 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); //视频流socket
try
controlSocket = connect(SOCKET_NAME); //控制指令socket
catch (IOException | RuntimeException e)
videoSocket.close();
throw e;
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
Size videoSize = device.getScreenInfo().getVideoSize();
//往视频流socket上发送设备名,宽度,高度
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
开启指令流监听
private static void startController(final Controller controller)
new Thread(new Runnable()
@Override
public void run()
try
controller.control(); //control()函数
catch (IOException e)
// this is expected on close
Ln.d("Controller stopped");
).start();
control()函数
public void control() throws IOException
// on start, power on the device
if (!device.isScreenOn())
device.injectKeycode(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();
事件类型处理
private void handleEvent() throws IOException
ControlMessage msg = connection.receiveControlMessage(); //接收控制流消息
switch (msg.getType())
case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents())
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
break;
case ControlMessage.TYPE_INJECT_TEXT:
if (device.supportsInputEvents())
injectText(msg.getText());
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents())
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: //以屏幕滚动事件为例
if (device.supportsInputEvents())
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (device.supportsInputEvents())
pressBackOrTurnScreenOn();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
if (clipboardText != null)
sender.pushClipboardText(clipboardText); //剪切板事件
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
setClipboard(msg.getText(), paste);
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents())
int mode = msg.getAction();
boolean setPowerModeOk = device.setScreenPowerMode(mode);
if (setPowerModeOk)
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice();
break;
default:
// do nothing
接收控制流消息
public ControlMessage receiveControlMessage() throws IOException
ControlMessage msg = reader.next();
while (msg == null)
reader.readFrom(controlInputStream); //向控制流的输入通道内读取数据
msg = reader.next(); //循环读取
return msg;
以屏幕滚动事件为例
private boolean injectScroll(Position position, int hScroll, int vScroll)
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
if (point == null)
// ignore event
return false;
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
//事件类型为ACTION_SCROLL
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event); //事件注入机制
事件注入机制
public boolean injectEvent(InputEvent inputEvent, int mode)
if (!supportsInputEvents())
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId))
return false;
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
使用了安卓内核的事件注入机制;
到此,控制流server端分析完
private Method getInjectInputEventMethod() throws NoSuchMethodException
if (injectInputEventMethod == null)
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
return injectInputEventMethod;
监听剪切板事件
private static void startDeviceMessageSender(final DeviceMessageSender sender)
new Thread(new Runnable()
@Override
public void run()
try
sender.loop();
catch (IOException | InterruptedException e)
// this is expected on close
Ln.d("Device message sender stopped");
).start();
循环监听
public void loop() throws IOException, InterruptedException
while (true)
String text;
synchronized (this)
while (clipboardText == null)
wait();
text = clipboardText;
clipboardText = null;
DeviceMessage event = DeviceMessage.createClipboard(text); //安卓剪切板事件
connection.sendDeviceMessage(event);
视频流相关逻辑
视频流编码
public void streamScreen(Device device, FileDescriptor fd) throws IOException
Workarounds.prepareMainLooper();
try
internalStreamScreen(device, fd);
catch (NullPointerException e)
// Retry with workarounds enabled:
// <https://github.com/Genymobile/scrcpy/issues/365>
// <https://github.com/Genymobile/scrcpy/issues/940>
Ln.d("Applying workarounds to avoid NullPointerException");
Workarounds.fillAppInfo();
internalStreamScreen(device, fd);
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this);
boolean alive;
try
do
MediaCodec codec = createCodec(); //创建编码器-h264 MediaFormat.MIMETYPE_VIDEO_AVC
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format); //配置编码器,包括码流,fps,I帧时间间隔等
Surface surface = codec.createInputSurface(); //创建输入缓冲区
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
try
alive = encode(codec, fd); //屏幕编码
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
finally
destroyDisplay(display);
codec.release();
surface.release();
while (alive);
finally
device.setRotationListener(null);
屏幕编码
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); //编码缓冲区
while (!consumeRotationChange() && !eof)
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); //获取输出区的缓冲的索引
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try
if (consumeRotationChange())
// must restart encoding with new size
break;
if (outputBufferId >= 0)
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); //根据索引获取数据
if (sendFrameMeta)
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); //先将帧元数据信息发送
IO.writeFully(fd, codecBuffer); //对外发送编码后的帧数据
finally
if (outputBufferId >= 0)
codec.releaseOutputBuffer(outputBufferId, false); //根据索引释放缓冲区
return !eof;
以上是关于scrcpy服务端代码分析的主要内容,如果未能解决你的问题,请参考以下文章