Compare commits

..

6 Commits

Author SHA1 Message Date
Romain Vimont
0969f4eae7 clock_noslope 2023-03-27 03:12:26 +02:00
Romain Vimont
2fff9b9edf Adapt FakeContext for Android 14
This fixes audio for Android 14 developer preview 2.

Fixes #3784 <https://github.com/Genymobile/scrcpy/issues/3784>

Suggested-by: Namelesswonder <Namelesswonder@users.noreply.github.com>
2023-03-20 08:40:34 +01:00
Romain Vimont
57f879d68a Adapt clipboard wrappers to Android 14
A new deviceId parameter has been added.

Fixes #3784 <https://github.com/Genymobile/scrcpy/issues/3784>
2023-03-20 08:40:25 +01:00
Romain Vimont
3626d90004 Use separate audio capture code for Android 11
The code to start audio capture is more complicated for Android 11
(launch a fake popup, wait, make several attempts, close the shell
package).

Use a distinct code path specific to Android 11.
2023-03-17 21:30:04 +01:00
Romain Vimont
02f4ff7534 Make 3 attempts to start AudioRecord
On Android 11, a fake popup must be briefly opened to make the system
think that the shell app is in the foreground so that audio may be
recorded.

Making the shell app foreground may take some time depending on the
device, so make 3 attempts, waiting 100ms before each.

Fixes #3796 <https://github.com/Genymobile/scrcpy/issues/3796>
2023-03-17 21:30:04 +01:00
Romain Vimont
a3871130cc List available encoders on failure
When the creation of an encoder fails, log an explicit error message
with the list of available encoders.
2023-03-17 21:29:45 +01:00
8 changed files with 103 additions and 242 deletions

View File

@@ -277,10 +277,6 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c', 'src/util/strbuf.c',
'src/util/term.c', 'src/util/term.c',
]], ]],
['test_clock', [
'tests/test_clock.c',
'src/clock.c',
]],
['test_control_msg_serialize', [ ['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c', 'tests/test_control_msg_serialize.c',
'src/control_msg.c', 'src/control_msg.c',

View File

@@ -1,116 +1,34 @@
#include "clock.h" #include "clock.h"
#include <assert.h>
#include "util/log.h" #include "util/log.h"
#define SC_CLOCK_NDEBUG // comment to debug #define SC_CLOCK_NDEBUG // comment to debug
void void
sc_clock_init(struct sc_clock *clock) { sc_clock_init(struct sc_clock *clock) {
clock->count = 0; clock->range = 0;
clock->head = 0; clock->offset = 0;
clock->left_sum.system = 0;
clock->left_sum.stream = 0;
clock->right_sum.system = 0;
clock->right_sum.stream = 0;
}
// Estimate the affine function f(stream) = slope * stream + offset
static void
sc_clock_estimate(struct sc_clock *clock,
double *out_slope, sc_tick *out_offset) {
assert(clock->count);
if (clock->count == 1) {
// If there is only 1 point, we can't compute a slope. Assume it is 1.
struct sc_clock_point *single_point = &clock->right_sum;
*out_slope = 1;
*out_offset = single_point->system - single_point->stream;
return;
}
struct sc_clock_point left_avg = {
.system = clock->left_sum.system / (clock->count / 2),
.stream = clock->left_sum.stream / (clock->count / 2),
};
struct sc_clock_point right_avg = {
.system = clock->right_sum.system / ((clock->count + 1) / 2),
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
};
double slope = (double) (right_avg.system - left_avg.system)
/ (right_avg.stream - left_avg.stream);
if (clock->count < SC_CLOCK_RANGE) {
/* The first frames are typically received and decoded with more delay
* than the others, causing a wrong slope estimation on start. To
* compensate, assume an initial slope of 1, then progressively use the
* estimated slope. */
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
/ SC_CLOCK_RANGE;
}
struct sc_clock_point global_avg = {
.system = (clock->left_sum.system + clock->right_sum.system)
/ clock->count,
.stream = (clock->left_sum.stream + clock->right_sum.stream)
/ clock->count,
};
sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
*out_slope = slope;
*out_offset = offset;
} }
void void
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
struct sc_clock_point *point = &clock->points[clock->head]; if (clock->range < SC_CLOCK_RANGE) {
++clock->range;
if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
// One point passes from the right sum to the left sum
unsigned mid;
if (clock->count == SC_CLOCK_RANGE) {
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
} else {
// Only for the first frames
mid = clock->count / 2;
}
struct sc_clock_point *mid_point = &clock->points[mid];
clock->left_sum.system += mid_point->system;
clock->left_sum.stream += mid_point->stream;
clock->right_sum.system -= mid_point->system;
clock->right_sum.stream -= mid_point->stream;
} }
if (clock->count == SC_CLOCK_RANGE) { sc_tick offset = system - stream;
// The current point overwrites the previous value in the circular clock->offset = ((clock->range - 1) * clock->offset + offset)
// array, update the left sum accordingly / clock->range;
clock->left_sum.system -= point->system;
clock->left_sum.stream -= point->stream;
} else {
++clock->count;
}
point->system = system;
point->stream = stream;
clock->right_sum.system += system;
clock->right_sum.stream += stream;
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
// Update estimation
sc_clock_estimate(clock, &clock->slope, &clock->offset);
#ifndef SC_CLOCK_NDEBUG #ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset); LOGD("Clock estimation: pts + %" PRItick, clock->offset);
#endif #endif
} }
sc_tick sc_tick
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
assert(clock->count); // sc_clock_update() must have been called assert(clock->range); // sc_clock_update() must have been called
return (sc_tick) (stream * clock->slope) + clock->offset; return stream + clock->offset;
} }

View File

@@ -3,12 +3,9 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include "util/tick.h" #include "util/tick.h"
#define SC_CLOCK_RANGE 32 #define SC_CLOCK_RANGE 32
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
struct sc_clock_point { struct sc_clock_point {
sc_tick system; sc_tick system;
@@ -21,40 +18,18 @@ struct sc_clock_point {
* *
* f(stream) = slope * stream + offset * f(stream) = slope * stream + offset
* *
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps * Theoretically, the slope encodes the drift between the device clock and the
* of a frame expressed both in stream time and system time) in a circular * computer clock. It is expected to be very close to 1.
* array.
* *
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two * Since the clock is used to estimate very close points in the future (which
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average * are reestimated on every clock update, see delay_buffer), the error caused
* point"). The slope of the estimated affine function is that of the line * by clock drift is totally negligible, so it is better to assume that the
* passing through these two points. * slope is 1 than to estimate it (the estimation error would be larger).
* *
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE * Therefore, only the offset is estimated.
* points. The resulting affine function passes by this centroid.
*
* With a circular array, the rolling sums (and average) are quick to compute.
* In practice, the estimation is stable and the evolution is smooth.
*/ */
struct sc_clock { struct sc_clock {
// Circular array unsigned range;
struct sc_clock_point points[SC_CLOCK_RANGE];
// Number of points in the array (count <= SC_CLOCK_RANGE)
unsigned count;
// Index of the next point to write
unsigned head;
// Sum of the first count/2 points
struct sc_clock_point left_sum;
// Sum of the last (count+1)/2 points
struct sc_clock_point right_sum;
// Estimated slope and offset
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
double slope;
sc_tick offset; sc_tick offset;
}; };

View File

@@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
sc_clock_update(&db->clock, sc_tick_now(), pts); sc_clock_update(&db->clock, sc_tick_now(), pts);
sc_cond_signal(&db->wait_cond); sc_cond_signal(&db->wait_cond);
if (db->first_frame_asap && db->clock.count == 1) { if (db->first_frame_asap && db->clock.range == 1) {
sc_mutex_unlock(&db->mutex); sc_mutex_unlock(&db->mutex);
return sc_frame_source_sinks_push(&db->frame_source, frame); return sc_frame_source_sinks_push(&db->frame_source, frame);
} }

View File

@@ -1,79 +0,0 @@
#include "common.h"
#include <assert.h>
#include "clock.h"
void test_small_rolling_sum(void) {
struct sc_clock clock;
sc_clock_init(&clock);
assert(clock.count == 0);
assert(clock.left_sum.system == 0);
assert(clock.left_sum.stream == 0);
assert(clock.right_sum.system == 0);
assert(clock.right_sum.stream == 0);
sc_clock_update(&clock, 2, 3);
assert(clock.count == 1);
assert(clock.left_sum.system == 0);
assert(clock.left_sum.stream == 0);
assert(clock.right_sum.system == 2);
assert(clock.right_sum.stream == 3);
sc_clock_update(&clock, 10, 20);
assert(clock.count == 2);
assert(clock.left_sum.system == 2);
assert(clock.left_sum.stream == 3);
assert(clock.right_sum.system == 10);
assert(clock.right_sum.stream == 20);
sc_clock_update(&clock, 40, 80);
assert(clock.count == 3);
assert(clock.left_sum.system == 2);
assert(clock.left_sum.stream == 3);
assert(clock.right_sum.system == 50);
assert(clock.right_sum.stream == 100);
sc_clock_update(&clock, 400, 800);
assert(clock.count == 4);
assert(clock.left_sum.system == 12);
assert(clock.left_sum.stream == 23);
assert(clock.right_sum.system == 440);
assert(clock.right_sum.stream == 880);
}
void test_large_rolling_sum(void) {
const unsigned half_range = SC_CLOCK_RANGE / 2;
struct sc_clock clock1;
sc_clock_init(&clock1);
for (unsigned i = 0; i < 5 * half_range; ++i) {
sc_clock_update(&clock1, i, 2 * i + 1);
}
struct sc_clock clock2;
sc_clock_init(&clock2);
for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
sc_clock_update(&clock2, i, 2 * i + 1);
}
assert(clock1.count == SC_CLOCK_RANGE);
assert(clock2.count == SC_CLOCK_RANGE);
// The values before the last SC_CLOCK_RANGE points in clock1 should have
// no impact
assert(clock1.left_sum.system == clock2.left_sum.system);
assert(clock1.left_sum.stream == clock2.left_sum.stream);
assert(clock1.right_sum.system == clock2.right_sum.system);
assert(clock1.right_sum.stream == clock2.right_sum.stream);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_small_rolling_sum();
test_large_rolling_sum();
return 0;
};

View File

@@ -274,7 +274,7 @@ public final class AudioEncoder implements AsyncProcessor {
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName); throw new ConfigurationException("Unknown encoder: " + encoderName);
} catch (IOException e) { } catch (IOException e) {
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
throw e; throw e;
} }
} }

View File

@@ -38,4 +38,10 @@ public final class FakeContext extends ContextWrapper {
builder.setPackageName(PACKAGE_NAME); builder.setPackageName(PACKAGE_NAME);
return builder.build(); return builder.build();
} }
// @Override to be added on SDK upgrade for Android 14
@SuppressWarnings("unused")
public int getDeviceId() {
return 0;
}
} }

View File

@@ -16,9 +16,9 @@ public class ClipboardManager {
private Method getPrimaryClipMethod; private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod; private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener; private Method addPrimaryClipChangedListener;
private boolean alternativeGetMethod; private int getMethodVersion;
private boolean alternativeSetMethod; private int setMethodVersion;
private boolean alternativeAddListenerMethod; private int addListenerMethodVersion;
public ClipboardManager(IInterface manager) { public ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
@@ -31,9 +31,15 @@ public class ClipboardManager {
} else { } else {
try { try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
} catch (NoSuchMethodException e) { getMethodVersion = 0;
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); } catch (NoSuchMethodException e1) {
alternativeGetMethod = true; try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
} catch (NoSuchMethodException e2) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
}
} }
} }
} }
@@ -47,41 +53,62 @@ public class ClipboardManager {
} else { } else {
try { try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
} catch (NoSuchMethodException e) { setMethodVersion = 0;
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); } catch (NoSuchMethodException e1) {
alternativeSetMethod = true; try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
} catch (NoSuchMethodException e2) {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
}
} }
} }
} }
return setPrimaryClipMethod; return setPrimaryClipMethod;
} }
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager) private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
throws InvocationTargetException, IllegalAccessException { throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
} }
if (alternativeMethod) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); switch (methodVersion) {
case 0:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
case 1:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
} }
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
} }
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException { throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
} else if (alternativeMethod) { return;
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); }
} else {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); switch (methodVersion) {
case 0:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
} }
} }
public CharSequence getText() { public CharSequence getText() {
try { try {
Method method = getGetPrimaryClipMethod(); Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager); ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
@@ -96,7 +123,7 @@ public class ClipboardManager {
try { try {
Method method = getSetPrimaryClipMethod(); Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, alternativeSetMethod, manager, clipData); setPrimaryClip(method, setMethodVersion, manager, clipData);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
@@ -104,14 +131,23 @@ public class ClipboardManager {
} }
} }
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager, private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME); method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
} else if (alternativeMethod) { return;
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); }
} else {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); switch (methodVersion) {
case 0:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
} }
} }
@@ -124,10 +160,19 @@ public class ClipboardManager {
try { try {
addPrimaryClipChangedListener = manager.getClass() addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
} catch (NoSuchMethodException e) { addListenerMethodVersion = 0;
addPrimaryClipChangedListener = manager.getClass() } catch (NoSuchMethodException e1) {
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class); try {
alternativeAddListenerMethod = true; addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class);
addListenerMethodVersion = 1;
} catch (NoSuchMethodException e2) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class, int.class);
addListenerMethodVersion = 2;
}
} }
} }
} }
@@ -137,7 +182,7 @@ public class ClipboardManager {
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try { try {
Method method = getAddPrimaryClipChangedListener(); Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener); addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);