Compare commits

..

2 Commits

Author SHA1 Message Date
Romain Vimont
b0a4e6df25 Retry on recoverable MediaCodec errors
Refs <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
Fixes #3693 <https://github.com/Genymobile/scrcpy/issues/3693>
2023-01-27 23:15:44 +01:00
Romain Vimont
545a8a8f32 Extract downsize-retry handling
Move the code to downscale and retry on error out of the catch-block.

Refs 26b4104844
2023-01-27 23:15:03 +01:00
20 changed files with 137 additions and 351 deletions

View File

@@ -26,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
Default is 8000000.
.TP
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
.BI "\-\-codec\-options " key[:type]=value[,...]
Set a list of comma-separated key:type=value options for the device encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
@@ -117,7 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
.BI "\-\-lock\-video\-orientation[=value]
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
Default is "unlocked".
@@ -199,7 +199,7 @@ It may only work over USB.
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
.TP
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
.BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen.
Default is 27183:27199.
@@ -260,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
.BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
@@ -270,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
.BI "\-\-tcpip[=ip[:port]]
Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).

View File

@@ -117,9 +117,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
uint16_t pressure =
sc_float_to_u16fp(msg->inject_touch_event.pressure);
sc_write16be(&buf[22], pressure);
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
return 32;
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
int16_t hscroll =
@@ -180,25 +179,22 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
if (pointer_name) {
// string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%f action_button=%06lx buttons=%06lx",
" pressure=%f buttons=%06lx",
pointer_name,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
} else {
// numeric pointer id
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%f action_button=%06lx"
" buttons=%06lx",
PRIi32 " pressure=%f buttons=%06lx",
id,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
}
break;

View File

@@ -65,7 +65,6 @@ struct sc_control_msg {
} inject_text;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons action_button;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
struct sc_position position;

View File

@@ -339,7 +339,6 @@ simulate_virtual_finger(struct sc_input_manager *im,
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0;
if (!sc_controller_push_msg(im->controller, &msg)) {

View File

@@ -93,7 +93,6 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.pointer_id = event->pointer_id,
.position = event->position,
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
.action_button = convert_mouse_buttons(event->button),
.buttons = convert_mouse_buttons(event->buttons_state),
},
};

View File

@@ -11,8 +11,6 @@
/** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
#define SC_PTS_ORIGIN_NONE UINT64_C(-1)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat *
@@ -171,18 +169,6 @@ run_recorder(void *data) {
sc_mutex_unlock(&recorder->mutex);
if (recorder->pts_origin == SC_PTS_ORIGIN_NONE
&& rec->packet->pts != AV_NOPTS_VALUE) {
// First PTS received
recorder->pts_origin = rec->packet->pts;
}
if (rec->packet->pts != AV_NOPTS_VALUE) {
// Set PTS relatve to the origin
rec->packet->pts -= recorder->pts_origin;
rec->packet->dts = rec->packet->pts;
}
// recorder->previous is only written from this thread, no need to lock
struct sc_record_packet *previous = recorder->previous;
recorder->previous = rec;
@@ -257,7 +243,6 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
recorder->failed = false;
recorder->header_written = false;
recorder->previous = NULL;
recorder->pts_origin = SC_PTS_ORIGIN_NONE;
const char *format_name = sc_recorder_get_format_name(recorder->format);
assert(format_name);

View File

@@ -28,8 +28,6 @@ struct sc_recorder {
struct sc_size declared_frame_size;
bool header_written;
uint64_t pts_origin;
sc_thread thread;
sc_mutex mutex;
sc_cond queue_cond;

View File

@@ -8,7 +8,6 @@
#include <SDL2/SDL_platform.h>
#include "adb/adb.h"
#include "util/binary.h"
#include "util/file.h"
#include "util/log.h"
#include "util/net_intr.h"
@@ -399,9 +398,10 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
info->frame_size.width = sc_read16be(fields);
info->frame_size.height = sc_read16be(&fields[2]);
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
return true;
}

View File

@@ -90,14 +90,13 @@ static void test_serialize_inject_touch_event(void) {
},
},
.pressure = 1.0f,
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
},
};
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 32);
assert(size == 28);
const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@@ -106,8 +105,7 @@ static void test_serialize_inject_touch_event(void) {
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
};
assert(!memcmp(buf, expected, sizeof(expected)));
}

View File

@@ -29,7 +29,6 @@ public final class ControlMessage {
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
@@ -61,15 +60,13 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
int buttons) {
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.actionButton = actionButton;
msg.buttons = buttons;
return msg;
}
@@ -143,10 +140,6 @@ public final class ControlMessage {
return keycode;
}
public int getActionButton() {
return actionButton;
}
public int getButtons() {
return buttons;
}

View File

@@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
@@ -140,9 +140,8 @@ public class ControlMessageReader {
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int actionButton = buffer.getInt();
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectScrollEvent() {

View File

@@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.Build;
import android.os.SystemClock;
import android.view.InputDevice;
@@ -24,8 +22,6 @@ public class Controller {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private Thread thread;
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
@@ -64,7 +60,7 @@ public class Controller {
}
}
private void control() throws IOException {
public void control() throws IOException {
// on start, power on the device
if (powerOn && !Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
@@ -84,27 +80,6 @@ public class Controller {
}
}
public void start() {
thread = new Thread(() -> {
try {
control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
});
thread.start();
sender.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
thread = null;
}
sender.stop();
}
public DeviceMessageSender getSender() {
return sender;
}
@@ -124,7 +99,7 @@ public class Controller {
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
@@ -204,7 +179,7 @@ public class Controller {
return successCount;
}
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
@@ -221,23 +196,22 @@ public class Controller {
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int source;
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);
} else {
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
source = InputDevice.SOURCE_TOUCHSCREEN;
// Buttons must not be set for touch events
buttons = 0;
pointer.setUp(action == MotionEvent.ACTION_UP);
}
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
@@ -251,62 +225,6 @@ public class Controller {
}
}
/* If the input device is a mouse (on API >= 23):
* - the first button pressed must first generate ACTION_DOWN;
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
* - the last button released must in addition generate ACTION_UP.
*
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
if (action == MotionEvent.ACTION_DOWN) {
if (actionButton == buttons) {
// First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
// Any button pressed: ACTION_BUTTON_PRESS
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false;
}
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
return true;
}
if (action == MotionEvent.ACTION_UP) {
// Any button released: ACTION_BUTTON_RELEASE
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false;
}
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
if (buttons == 0) {
// Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
return true;
}
}
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);

View File

@@ -277,26 +277,6 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {
Ln.e("No display ids detected");
return false;
}
boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
}
return allOk;
}
// Older Android versions, only 1 display
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");

View File

@@ -6,8 +6,6 @@ public final class DeviceMessageSender {
private final DesktopConnection connection;
private Thread thread;
private String clipboardText;
private long ack;
@@ -26,7 +24,7 @@ public final class DeviceMessageSender {
notify();
}
private void loop() throws IOException, InterruptedException {
public void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) {
String text;
long sequence;
@@ -51,22 +49,4 @@ public final class DeviceMessageSender {
}
}
}
public void start() {
thread = new Thread(() -> {
try {
loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
thread = null;
}
}
}

View File

@@ -12,6 +12,7 @@ import android.os.IBinder;
import android.os.SystemClock;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -21,30 +22,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
public interface Callbacks {
void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException;
}
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
// Keep the values in descending order
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private final String encoderName;
private final List<CodecOption> codecOptions;
private final int bitRate;
private final int maxFps;
private final boolean sendFrameMeta;
private final boolean downsizeOnError;
private long ptsOrigin;
private boolean firstFrameSent;
private int consecutiveErrors;
public ScreenEncoder(int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
@@ -61,7 +64,18 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false);
}
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
IBinder display = createDisplay();
@@ -90,15 +104,25 @@ public class ScreenEncoder implements Device.RotationListener {
codec.start();
alive = encode(codec, callbacks);
alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} catch (MediaCodec.CodecException e) {
Ln.e("Codec error: " + e.getMessage());
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
// For simplicity, handle isTransient() like isRecoverable()
if (e.isRecoverable() || e.isTransient()) {
// Avoid busy-loop if too many errors are generated
SystemClock.sleep(50);
} else if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
alive = true;
} catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareRetry(device, screenInfo)) {
if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
Ln.i("Retrying...");
alive = true;
} finally {
codec.reset();
@@ -114,26 +138,13 @@ public class ScreenEncoder implements Device.RotationListener {
}
}
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
if (firstFrameSent) {
++consecutiveErrors;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
// Definitively fail
return false;
}
// Wait a bit to increase the probability that retrying will fix the problem
SystemClock.sleep(50);
return true;
}
if (!downsizeOnError) {
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
if (!downsizeOnError || firstFrameSent) {
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
// Must fail immediately
return false;
}
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) {
@@ -159,30 +170,30 @@ public class ScreenEncoder implements Device.RotationListener {
return 0;
}
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
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;
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
if (!isConfig) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
consecutiveErrors = 0;
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
callbacks.onPacket(codecBuffer, bufferInfo);
IO.writeFully(fd, codecBuffer);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
}
}
} finally {
if (outputBufferId >= 0) {
@@ -194,6 +205,28 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof;
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = PACKET_FLAG_CONFIG; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
pts |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodecInfo[] listEncoders() {
List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);

View File

@@ -71,47 +71,39 @@ public final class Server {
boolean control = options.getControl();
boolean sendDummyByte = options.getSendDummyByte();
Workarounds.prepareMainLooper();
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
Workarounds.fillAppInfo();
}
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
}
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(),
options.getDownsizeOnError());
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName(), options.getDownsizeOnError());
Controller controller = null;
Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (control) {
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller.start();
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
final Controller controllerRef = controller;
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
// asynchronous
controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
}
try {
// synchronous
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
screenEncoder.streamScreen(device, videoStreamer);
screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controller != null) {
controller.stop();
if (controllerThread != null) {
controllerThread.interrupt();
}
if (deviceMessageSenderThread != null) {
deviceMessageSenderThread.interrupt();
}
}
}
@@ -123,6 +115,32 @@ public final class Server {
return thread;
}
private static Thread startController(final Controller controller) {
Thread thread = new Thread(() -> {
try {
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
});
thread.start();
return thread;
}
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(() -> {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
});
thread.start();
return thread;
}
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");

View File

@@ -1,51 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class VideoStreamer implements ScreenEncoder.Callbacks {
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private final FileDescriptor fd;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
this.fd = fd;
this.sendFrameMeta = sendFrameMeta;
}
@Override
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long ptsAndFlags;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
} else {
ptsAndFlags = bufferInfo.presentationTimeUs;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(ptsAndFlags);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
}

View File

@@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.view.InputEvent;
import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -18,7 +17,6 @@ public final class InputManager {
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
public InputManager(android.hardware.input.InputManager manager) {
this.manager = manager;
@@ -58,22 +56,4 @@ public final class InputManager {
return false;
}
}
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
if (setActionButtonMethod == null) {
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
}
return setActionButtonMethod;
}
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
try {
Method method = getSetActionButtonMethod();
method.invoke(motionEvent, actionButton);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot set action button on MotionEvent", e);
return false;
}
}
}

View File

@@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
@@ -31,8 +30,6 @@ public final class SurfaceControl {
private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod;
private static Method getPhysicalDisplayTokenMethod;
private static Method getPhysicalDisplayIdsMethod;
private SurfaceControl() {
// only static methods
@@ -101,6 +98,7 @@ public final class SurfaceControl {
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -116,40 +114,6 @@ public final class SurfaceControl {
}
}
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
if (getPhysicalDisplayTokenMethod == null) {
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
}
return getPhysicalDisplayTokenMethod;
}
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
try {
Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
if (getPhysicalDisplayIdsMethod == null) {
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
}
return getPhysicalDisplayIdsMethod;
}
public static long[] getPhysicalDisplayIds() {
try {
Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);

View File

@@ -94,8 +94,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
byte[] packet = bos.toByteArray();
@@ -113,7 +112,6 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
}