Compare commits

..

10 Commits

Author SHA1 Message Date
Romain Vimont
86e5c90ed6 Add workarounds for Honor devices
Audio did not work on Honor devices.

Two workarounds are necessary:
 - a system context must be set as a base context of FakeContext (so
   that a PackageManager is available),
 - the same workaround as for Meizu phones must also be applied (so that
   ActivityThread.currentApplication() return a valid instance).

These workarounds must not be applied for all devices, they cause other
issues.

Fixes #4015 <https://github.com/Genymobile/scrcpy/issues/4015>
Refs #3085 <https://github.com/Genymobile/scrcpy/issues/3805>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-06-18 17:57:42 +02:00
Romain Vimont
48a00fb481 Log device BRAND
The BRAND value is not always the same as the MANUFACTURER value.
2023-06-17 00:25:01 +02:00
Romain Vimont
3b7e2ca9c8 Fix lint warning
Suppress lint "DiscouragedPrivateApi" in Workarounds.java.
2023-06-16 23:24:08 +02:00
wuderek
5bd7514871 Add InputManagerGlobal for Android 14 beta 3
Parts of the InputManager class have been moved to a new
InputManagerGlobal class in Android 14 preview.

Fixes #4074 <https://github.com/Genymobile/scrcpy/issues/4074>
PR #4075 <https://github.com/Genymobile/scrcpy/pull/4075>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-06-13 15:19:24 +02:00
Romain Vimont
d3c2955fb9 Add --time-limit
Add an option to stop scrcpy automatically after a given delay.

PR #4052 <https://github.com/Genymobile/scrcpy/pull/4052>
Fixes #3752 <https://github.com/Genymobile/scrcpy/issues/3752>
2023-06-10 16:04:51 +02:00
Romain Vimont
5042f8de93 Improve recording documentation 2023-06-10 16:03:27 +02:00
Romain Vimont
7536f95d1c Rename raw_video_stream to raw_stream
This server-specific option impacts both the video and audio streams.
2023-06-10 12:09:43 +02:00
Romain Vimont
6832e8d629 Remove spurious empty line 2023-06-10 12:07:35 +02:00
Romain Vimont
28313631e5 Reformat Java code
Fix code style.
2023-06-09 22:28:01 +02:00
Romain Vimont
fdbc9397a7 Name Java threads
Give a user-friendly name to Java threads created by the server.
2023-06-09 22:27:35 +02:00
23 changed files with 295 additions and 37 deletions

View File

@@ -60,6 +60,7 @@ _scrcpy() {
-t --show-touches
--tcpip
--tcpip=
--time-limit=
--tunnel-host=
--tunnel-port=
--v4l2-buffer=

View File

@@ -65,6 +65,7 @@ arguments=(
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
{-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
'--time-limit=[Set the maximum mirroring time, in seconds]'
'--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]'
'--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]'
'--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'

View File

@@ -51,6 +51,7 @@ src = [
'src/util/term.c',
'src/util/thread.c',
'src/util/tick.c',
'src/util/timeout.c',
]
conf = configuration_data()

View File

@@ -354,6 +354,10 @@ If a destination address is provided, then scrcpy connects to this address befor
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
.TP
.BI "\-\-time\-limit " seconds
Set the maximum mirroring time, in seconds.
.TP
.BI "\-\-tunnel\-host " ip
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward.

View File

@@ -78,6 +78,7 @@ enum {
OPT_NO_VIDEO_PLAYBACK,
OPT_AUDIO_SOURCE,
OPT_KILL_ADB_ON_CLOSE,
OPT_TIME_LIMIT,
};
struct sc_option {
@@ -580,6 +581,12 @@ static const struct sc_option options[] = {
"connected over USB), enables TCP/IP mode, then connects to "
"this address before starting.",
},
{
.longopt_id = OPT_TIME_LIMIT,
.longopt = "time-limit",
.argdesc = "seconds",
.text = "Set the maximum mirroring time, in seconds.",
},
{
.longopt_id = OPT_TUNNEL_HOST,
.longopt = "tunnel-host",
@@ -1618,6 +1625,18 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return false;
}
static bool
parse_time_limit(const char *s, sc_tick *tick) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@@ -1953,6 +1972,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_KILL_ADB_ON_CLOSE:
opts->kill_adb_on_close = true;
break;
case OPT_TIME_LIMIT:
if (!parse_time_limit(optarg, &opts->time_limit)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;

View File

@@ -6,3 +6,4 @@
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)

View File

@@ -42,6 +42,7 @@ const struct scrcpy_options scrcpy_options_default = {
.display_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,

View File

@@ -142,6 +142,7 @@ struct scrcpy_options {
sc_tick display_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
sc_tick time_limit;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;

View File

@@ -35,6 +35,7 @@
#include "util/log.h"
#include "util/net.h"
#include "util/rand.h"
#include "util/timeout.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
@@ -73,6 +74,7 @@ struct scrcpy {
struct sc_hid_mouse mouse_hid;
#endif
};
struct sc_timeout timeout;
};
static inline void
@@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_TIME_LIMIT_REACHED:
LOGI("Time limit reached");
return SCRCPY_EXIT_SUCCESS;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
@@ -280,6 +285,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event
}
static void
sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) {
(void) timeout;
(void) userdata;
PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED);
}
// Generate a scrcpy id to differentiate multiple running scrcpy instances
static uint32_t
scrcpy_generate_scid() {
@@ -321,6 +334,8 @@ scrcpy(struct scrcpy_options *options) {
bool controller_initialized = false;
bool controller_started = false;
bool screen_initialized = false;
bool timeout_initialized = false;
bool timeout_started = false;
struct sc_acksync *acksync = NULL;
@@ -743,6 +758,27 @@ aoa_hid_end:
}
}
if (options->time_limit) {
bool ok = sc_timeout_init(&s->timeout);
if (!ok) {
goto end;
}
timeout_initialized = true;
sc_tick deadline = sc_tick_now() + options->time_limit;
static const struct sc_timeout_callbacks cbs = {
.on_timeout = sc_timeout_on_timeout,
};
ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL);
if (!ok) {
goto end;
}
timeout_started = true;
}
ret = event_loop(s);
LOGD("quit...");
@@ -751,6 +787,10 @@ aoa_hid_end:
sc_screen_hide_window(&s->screen);
end:
if (timeout_started) {
sc_timeout_stop(&s->timeout);
}
// The demuxer is not stopped explicitly, because it will stop by itself on
// end-of-stream
#ifdef HAVE_USB
@@ -786,6 +826,13 @@ end:
sc_server_stop(&s->server);
}
if (timeout_started) {
sc_timeout_join(&s->timeout);
}
if (timeout_initialized) {
sc_timeout_destroy(&s->timeout);
}
// now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them
if (video_demuxer_started) {

77
app/src/util/timeout.c Normal file
View File

@@ -0,0 +1,77 @@
#include "timeout.h"
#include <assert.h>
#include "log.h"
bool
sc_timeout_init(struct sc_timeout *timeout) {
bool ok = sc_mutex_init(&timeout->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&timeout->cond);
if (!ok) {
return false;
}
timeout->stopped = false;
return true;
}
static int
run_timeout(void *data) {
struct sc_timeout *timeout = data;
sc_tick deadline = timeout->deadline;
sc_mutex_lock(&timeout->mutex);
bool timed_out = false;
while (!timeout->stopped && !timed_out) {
timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex,
deadline);
}
sc_mutex_unlock(&timeout->mutex);
timeout->cbs->on_timeout(timeout, timeout->cbs_userdata);
return 0;
}
bool
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
const struct sc_timeout_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout",
timeout);
if (!ok) {
LOGE("Timeout: could not start thread");
return false;
}
timeout->deadline = deadline;
assert(cbs && cbs->on_timeout);
timeout->cbs = cbs;
timeout->cbs_userdata = cbs_userdata;
return true;
}
void
sc_timeout_stop(struct sc_timeout *timeout) {
sc_mutex_lock(&timeout->mutex);
timeout->stopped = true;
sc_mutex_unlock(&timeout->mutex);
}
void
sc_timeout_join(struct sc_timeout *timeout) {
sc_thread_join(&timeout->thread, NULL);
}
void
sc_timeout_destroy(struct sc_timeout *timeout) {
sc_mutex_destroy(&timeout->mutex);
sc_cond_destroy(&timeout->cond);
}

43
app/src/util/timeout.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef SC_TIMEOUT_H
#define SC_TIMEOUT_H
#include "common.h"
#include <stdbool.h>
#include "thread.h"
#include "tick.h"
struct sc_timeout {
sc_thread thread;
sc_tick deadline;
sc_mutex mutex;
sc_cond cond;
bool stopped;
const struct sc_timeout_callbacks *cbs;
void *cbs_userdata;
};
struct sc_timeout_callbacks {
void (*on_timeout)(struct sc_timeout *timeout, void *userdata);
};
bool
sc_timeout_init(struct sc_timeout *timeout);
void
sc_timeout_destroy(struct sc_timeout *timeout);
bool
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
const struct sc_timeout_callbacks *cbs, void *cbs_userdata);
void
sc_timeout_stop(struct sc_timeout *timeout);
void
sc_timeout_join(struct sc_timeout *timeout);
#endif

View File

@@ -21,20 +21,15 @@ scrcpy --no-video --audio-codec=aac --record=file.aac
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
```
To disable playback while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
Timestamps are captured on the device, so [packet delay variation] does not
impact the recorded file, which is always clean (only if you use `--record` of
course, not if you capture your scrcpy window and audio output on the computer).
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
## Format
The video and audio streams are encoded on the device, but are muxed on the
client side. Two formats (containers) are supported:
- Matroska (`.mkv`)
@@ -48,3 +43,36 @@ needs not end with `.mkv` or `.mp4`):
```
scrcpy --record=file --record-format=mkv
```
## No playback
To disable playback while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
It is also possible to disable video and audio playback separately:
```bash
# Record both video and audio, but only play video
scrcpy --record=file.mkv --no-audio-playback
```
## Time limit
To limit the recording time:
```bash
scrcpy --record=file.mkv --time-limit=20 # in seconds
```
The `--time-limit` option is not limited to recording, it also impacts simple
mirroring:
```
scrcpy --time-limit=20
```

View File

@@ -134,7 +134,7 @@ public final class AudioEncoder implements AsyncProcessor {
Ln.d("Audio encoder stopped");
listener.onTerminated(fatalError);
}
});
}, "audio-encoder");
thread.start();
}
@@ -183,7 +183,7 @@ public final class AudioEncoder implements AsyncProcessor {
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);
mediaCodecThread = new HandlerThread("AudioEncoder");
mediaCodecThread = new HandlerThread("media-codec");
mediaCodecThread.start();
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
@@ -201,7 +201,7 @@ public final class AudioEncoder implements AsyncProcessor {
} finally {
end();
}
});
}, "audio-in");
outputThread = new Thread(() -> {
try {
@@ -216,7 +216,7 @@ public final class AudioEncoder implements AsyncProcessor {
} finally {
end();
}
});
}, "audio-out");
mediaCodec.start();
mediaCodecStarted = true;

View File

@@ -69,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
Ln.d("Audio recorder stopped");
listener.onTerminated(fatalError);
}
});
}, "audio-raw");
thread.start();
}

View File

@@ -95,7 +95,7 @@ public class Controller implements AsyncProcessor {
Ln.d("Controller stopped");
listener.onTerminated(true);
}
});
}, "control-recv");
thread.start();
sender.start();
}

View File

@@ -60,7 +60,7 @@ public final class DeviceMessageSender {
} finally {
Ln.d("Device message sender stopped");
}
});
}, "control-send");
thread.start();
}

View File

@@ -2,11 +2,11 @@ package com.genymobile.scrcpy;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContextWrapper;
import android.content.MutableContextWrapper;
import android.os.Build;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
public final class FakeContext extends MutableContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29

View File

@@ -318,9 +318,9 @@ public class Options {
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
case "raw_stream":
boolean rawStream = Boolean.parseBoolean(value);
if (rawStream) {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;

View File

@@ -299,7 +299,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
Ln.d("Screen streaming stopped");
listener.onTerminated(true);
}
});
}, "video");
thread.start();
}

View File

@@ -87,7 +87,7 @@ public final class Server {
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
Thread initThread = startInitThread(options);
@@ -101,15 +101,23 @@ public final class Server {
Workarounds.prepareMainLooper();
// 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>
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();
} else if (Build.BRAND.equalsIgnoreCase("honor")) {
// Honor devices require both a system context (as a base context of FakeContext) and the same workarounds as for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/4015>
// The system context must not be set for all devices, it would cause other problems:
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
Workarounds.fillBaseContext();
Workarounds.fillAppInfo();
}
@@ -137,8 +145,7 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
options.getSendFrameMeta());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
@@ -185,7 +192,7 @@ public final class Server {
}
private static Thread startInitThread(final Options options) {
Thread thread = new Thread(() -> initAndCleanUp(options));
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
thread.start();
return thread;
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes;
@@ -22,6 +23,7 @@ public final class Workarounds {
private static Class<?> activityThreadClass;
private static Object activityThread;
private static boolean currentActivityThreadInitialized;
private Workarounds() {
// not instantiable
@@ -41,18 +43,26 @@ public final class Workarounds {
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
private static void initActivityThread() throws Exception {
if (activityThread == null) {
// ActivityThread activityThread = new ActivityThread();
activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
}
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
initActivityThread();
if (!currentActivityThreadInitialized) {
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
currentActivityThreadInitialized = true;
}
}
@@ -105,8 +115,21 @@ public final class Workarounds {
}
}
public static void fillBaseContext() {
try {
initActivityThread();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context context = (Context) getSystemContextMethod.invoke(activityThread);
FakeContext.get().setBaseContext(context);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.d("Could not fill base context: " + throwable.getMessage());
}
}
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
@SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//

View File

@@ -76,7 +76,7 @@ public final class ServiceManager {
try {
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null);
Object im = getInstanceMethod.invoke(null);
inputManager = new InputManager(im);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);

View File

@@ -12,7 +12,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlMessageReaderTest {
@Test