Compare commits
4 Commits
codec_opti
...
name_param
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
870ced088e | ||
|
|
a845ea0794 | ||
|
|
a0d98fe4ae | ||
|
|
74ece9b45b |
@@ -25,14 +25,6 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
|||||||
|
|
||||||
Default is 8000000.
|
Default is 8000000.
|
||||||
|
|
||||||
.TP
|
|
||||||
.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'.
|
|
||||||
The list of possible codec options is available in the Android documentation:
|
|
||||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
|
||||||
.UE .
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||||
Crop the device screen on the server.
|
Crop the device screen on the server.
|
||||||
|
|||||||
@@ -30,15 +30,6 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
" Default is %d.\n"
|
" Default is %d.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --codec-options key[:type]=value[,...]\n"
|
|
||||||
" Set a list of comma-separated key:type=value options for the\n"
|
|
||||||
" device encoder.\n"
|
|
||||||
" The possible values for 'type' are 'int' (default), 'long',\n"
|
|
||||||
" 'float' and 'string'.\n"
|
|
||||||
" The list of possible codec options is available in the\n"
|
|
||||||
" Android documentation:\n"
|
|
||||||
" <https://d.android.com/reference/android/media/MediaFormat>\n"
|
|
||||||
"\n"
|
|
||||||
" --crop width:height:x:y\n"
|
" --crop width:height:x:y\n"
|
||||||
" Crop the device screen on the server.\n"
|
" Crop the device screen on the server.\n"
|
||||||
" The values are expressed in the device natural orientation\n"
|
" The values are expressed in the device natural orientation\n"
|
||||||
@@ -481,14 +472,12 @@ guess_record_format(const char *filename) {
|
|||||||
#define OPT_ROTATION 1015
|
#define OPT_ROTATION 1015
|
||||||
#define OPT_RENDER_DRIVER 1016
|
#define OPT_RENDER_DRIVER 1016
|
||||||
#define OPT_NO_MIPMAPS 1017
|
#define OPT_NO_MIPMAPS 1017
|
||||||
#define OPT_CODEC_OPTIONS 1018
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
static const struct option long_options[] = {
|
static const struct option long_options[] = {
|
||||||
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
{"bit-rate", required_argument, NULL, 'b'},
|
||||||
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
|
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"crop", required_argument, NULL, OPT_CROP},
|
||||||
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
@@ -658,9 +647,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_NO_MIPMAPS:
|
case OPT_NO_MIPMAPS:
|
||||||
opts->mipmaps = false;
|
opts->mipmaps = false;
|
||||||
break;
|
break;
|
||||||
case OPT_CODEC_OPTIONS:
|
|
||||||
opts->codec_options = optarg;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
.display_id = options->display_id,
|
.display_id = options->display_id,
|
||||||
.show_touches = options->show_touches,
|
.show_touches = options->show_touches,
|
||||||
.stay_awake = options->stay_awake,
|
.stay_awake = options->stay_awake,
|
||||||
.codec_options = options->codec_options,
|
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ struct scrcpy_options {
|
|||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
const char *render_driver;
|
const char *render_driver;
|
||||||
const char *codec_options;
|
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
@@ -49,7 +48,6 @@ struct scrcpy_options {
|
|||||||
.window_title = NULL, \
|
.window_title = NULL, \
|
||||||
.push_target = NULL, \
|
.push_target = NULL, \
|
||||||
.render_driver = NULL, \
|
.render_driver = NULL, \
|
||||||
.codec_options = NULL, \
|
|
||||||
.record_format = RECORDER_FORMAT_AUTO, \
|
.record_format = RECORDER_FORMAT_AUTO, \
|
||||||
.port_range = { \
|
.port_range = { \
|
||||||
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
||||||
|
|||||||
@@ -231,22 +231,17 @@ enable_tunnel_any_port(struct server *server, struct port_range port_range) {
|
|||||||
|
|
||||||
static process_t
|
static process_t
|
||||||
execute_server(struct server *server, const struct server_params *params) {
|
execute_server(struct server *server, const struct server_params *params) {
|
||||||
char max_size_string[6];
|
process_t result = PROCESS_NONE;
|
||||||
char bit_rate_string[11];
|
|
||||||
char max_fps_string[6];
|
char *cmd[128];
|
||||||
char lock_video_orientation_string[3];
|
int i = 0;
|
||||||
char display_id_string[6];
|
cmd[i++] = "shell";
|
||||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
cmd[i++] = "CLASSPATH=" DEVICE_SERVER_PATH;
|
||||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
cmd[i++] = "app_process";
|
||||||
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
|
||||||
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
|
|
||||||
sprintf(display_id_string, "%"PRIu16, params->display_id);
|
|
||||||
const char *const cmd[] = {
|
|
||||||
"shell",
|
|
||||||
"CLASSPATH=" DEVICE_SERVER_PATH,
|
|
||||||
"app_process",
|
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
# define SERVER_DEBUGGER_PORT "5005"
|
# define SERVER_DEBUGGER_PORT "5005"
|
||||||
|
cmd[i++] =
|
||||||
# ifdef SERVER_DEBUGGER_METHOD_NEW
|
# ifdef SERVER_DEBUGGER_METHOD_NEW
|
||||||
/* Android 9 and above */
|
/* Android 9 and above */
|
||||||
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
|
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
|
||||||
@@ -254,24 +249,38 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
/* Android 8 and below */
|
/* Android 8 and below */
|
||||||
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
||||||
# endif
|
# endif
|
||||||
SERVER_DEBUGGER_PORT,
|
SERVER_DEBUGGER_PORT;
|
||||||
#endif
|
#endif
|
||||||
"/", // unused
|
|
||||||
"com.genymobile.scrcpy.Server",
|
cmd[i++] = "/"; // unused
|
||||||
SCRCPY_VERSION,
|
cmd[i++] = "com.genymobile.scrcpy.Server";
|
||||||
max_size_string,
|
cmd[i++] = SCRCPY_VERSION;
|
||||||
bit_rate_string,
|
|
||||||
max_fps_string,
|
int dyn_index = i; // from there, the strings are allocated
|
||||||
lock_video_orientation_string,
|
#define ADD_PARAM(fmt, ...) \
|
||||||
server->tunnel_forward ? "true" : "false",
|
cmd[i] = sc_asprintf(fmt, ## __VA_ARGS__); \
|
||||||
params->crop ? params->crop : "-",
|
if (!cmd[i++]) { \
|
||||||
"true", // always send frame meta (packet boundaries + timestamp)
|
goto end; \
|
||||||
params->control ? "true" : "false",
|
}
|
||||||
display_id_string,
|
|
||||||
params->show_touches ? "true" : "false",
|
#define STRBOOL(p) (p ? "true" : "false")
|
||||||
params->stay_awake ? "true" : "false",
|
|
||||||
params->codec_options ? params->codec_options : "-",
|
ADD_PARAM("max_size=%"PRIu16, params->max_size);
|
||||||
};
|
ADD_PARAM("bit_rate=%"PRIu32, params->bit_rate);
|
||||||
|
ADD_PARAM("max_fps=%"PRIu16, params->max_fps);
|
||||||
|
ADD_PARAM("lock_video_orientation=%"PRIi8, params->lock_video_orientation);
|
||||||
|
ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel_forward));
|
||||||
|
ADD_PARAM("crop=%s", params->crop ? params->crop : "");
|
||||||
|
// always send frame meta (packet boundaries + timestamp)
|
||||||
|
ADD_PARAM("send_frame_meta=true");
|
||||||
|
ADD_PARAM("control=%s", STRBOOL(params->control));
|
||||||
|
ADD_PARAM("display_id=%"PRIu16, params->display_id);
|
||||||
|
ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches));
|
||||||
|
ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake));
|
||||||
|
|
||||||
|
#undef ADD_PARAM
|
||||||
|
#undef STRBOOL
|
||||||
|
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
LOGI("Server debugger waiting for a client on device port "
|
LOGI("Server debugger waiting for a client on device port "
|
||||||
SERVER_DEBUGGER_PORT "...");
|
SERVER_DEBUGGER_PORT "...");
|
||||||
@@ -283,7 +292,14 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
// Port: 5005
|
// Port: 5005
|
||||||
// Then click on "Debug"
|
// Then click on "Debug"
|
||||||
#endif
|
#endif
|
||||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
result = adb_execute(server->serial, (const char **) cmd, i);
|
||||||
|
|
||||||
|
end:
|
||||||
|
for (int j = i; j > dyn_index; --j) {
|
||||||
|
free(cmd[j - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static socket_t
|
static socket_t
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ struct server {
|
|||||||
|
|
||||||
struct server_params {
|
struct server_params {
|
||||||
const char *crop;
|
const char *crop;
|
||||||
const char *codec_options;
|
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "str_util.h"
|
#include "str_util.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -195,3 +197,33 @@ utf8_from_wide_char(const wchar_t *ws) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
char *
|
||||||
|
sc_asprintf(const char *fmt, ...) {
|
||||||
|
va_list va;
|
||||||
|
va_start(va, fmt);
|
||||||
|
char *s = sc_vasprintf(fmt, va);
|
||||||
|
va_end(va);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *
|
||||||
|
sc_vasprintf(const char *fmt, va_list ap) {
|
||||||
|
va_list va;
|
||||||
|
va_copy(va, ap);
|
||||||
|
int len = vsnprintf(NULL, 0, fmt, va);
|
||||||
|
va_end(va);
|
||||||
|
|
||||||
|
char *str = malloc(len + 1);
|
||||||
|
if (!str) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
va_copy(va, ap);
|
||||||
|
int len2 = vsprintf(str, fmt, va);
|
||||||
|
(void) len2;
|
||||||
|
assert(len == len2);
|
||||||
|
va_end(va);
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#ifndef STRUTIL_H
|
#ifndef STRUTIL_H
|
||||||
#define STRUTIL_H
|
#define STRUTIL_H
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
@@ -57,4 +59,13 @@ char *
|
|||||||
utf8_from_wide_char(const wchar_t *s);
|
utf8_from_wide_char(const wchar_t *s);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// compatibility function similar to asprintf()
|
||||||
|
// (but returning the resulting string for convenience)
|
||||||
|
char *
|
||||||
|
sc_asprintf(const char *fmt, ...)
|
||||||
|
__attribute__((format(printf, 1, 2)));
|
||||||
|
|
||||||
|
char *
|
||||||
|
sc_vasprintf(const char *fmt, va_list ap);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class CodecOption {
|
|
||||||
private String key;
|
|
||||||
private Object value;
|
|
||||||
|
|
||||||
public CodecOption(String key, Object value) {
|
|
||||||
this.key = key;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKey() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getValue() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CodecOption> parse(String codecOptions) {
|
|
||||||
if ("-".equals(codecOptions)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CodecOption> result = new ArrayList<>();
|
|
||||||
|
|
||||||
boolean escape = false;
|
|
||||||
StringBuilder buf = new StringBuilder();
|
|
||||||
|
|
||||||
for (char c : codecOptions.toCharArray()) {
|
|
||||||
switch (c) {
|
|
||||||
case '\\':
|
|
||||||
if (escape) {
|
|
||||||
buf.append('\\');
|
|
||||||
escape = false;
|
|
||||||
} else {
|
|
||||||
escape = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ',':
|
|
||||||
if (escape) {
|
|
||||||
buf.append(',');
|
|
||||||
escape = false;
|
|
||||||
} else {
|
|
||||||
// This comma is a separator between codec options
|
|
||||||
String codecOption = buf.toString();
|
|
||||||
result.add(parseOption(codecOption));
|
|
||||||
// Clear buf
|
|
||||||
buf.setLength(0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
buf.append(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf.length() > 0) {
|
|
||||||
String codecOption = buf.toString();
|
|
||||||
result.add(parseOption(codecOption));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CodecOption parseOption(String option) {
|
|
||||||
int equalSignIndex = option.indexOf('=');
|
|
||||||
if (equalSignIndex == -1) {
|
|
||||||
throw new IllegalArgumentException("'=' expected");
|
|
||||||
}
|
|
||||||
String keyAndType = option.substring(0, equalSignIndex);
|
|
||||||
if (keyAndType.length() == 0) {
|
|
||||||
throw new IllegalArgumentException("Key may not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
String key;
|
|
||||||
String type;
|
|
||||||
|
|
||||||
int colonIndex = keyAndType.indexOf(':');
|
|
||||||
if (colonIndex != -1) {
|
|
||||||
key = keyAndType.substring(0, colonIndex);
|
|
||||||
type = keyAndType.substring(colonIndex + 1);
|
|
||||||
} else {
|
|
||||||
key = keyAndType;
|
|
||||||
type = "int"; // assume int by default
|
|
||||||
}
|
|
||||||
|
|
||||||
Object value;
|
|
||||||
String valueString = option.substring(equalSignIndex + 1);
|
|
||||||
switch (type) {
|
|
||||||
case "int":
|
|
||||||
value = Integer.parseInt(valueString);
|
|
||||||
break;
|
|
||||||
case "long":
|
|
||||||
value = Long.parseLong(valueString);
|
|
||||||
break;
|
|
||||||
case "float":
|
|
||||||
value = Float.parseFloat(valueString);
|
|
||||||
break;
|
|
||||||
case "string":
|
|
||||||
value = valueString;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CodecOption(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ public class Options {
|
|||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private int lockedVideoOrientation;
|
private int lockedVideoOrientation = -1;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
private Rect crop;
|
private Rect crop;
|
||||||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
||||||
@@ -14,7 +14,6 @@ public class Options {
|
|||||||
private int displayId;
|
private int displayId;
|
||||||
private boolean showTouches;
|
private boolean showTouches;
|
||||||
private boolean stayAwake;
|
private boolean stayAwake;
|
||||||
private String codecOptions;
|
|
||||||
|
|
||||||
public int getMaxSize() {
|
public int getMaxSize() {
|
||||||
return maxSize;
|
return maxSize;
|
||||||
@@ -103,12 +102,4 @@ public class Options {
|
|||||||
public void setStayAwake(boolean stayAwake) {
|
public void setStayAwake(boolean stayAwake) {
|
||||||
this.stayAwake = stayAwake;
|
this.stayAwake = stayAwake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCodecOptions() {
|
|
||||||
return codecOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCodecOptions(String codecOptions) {
|
|
||||||
this.codecOptions = codecOptions;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import android.view.Surface;
|
|||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
@@ -26,17 +25,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
private List<CodecOption> codecOptions;
|
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -64,7 +61,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL, codecOptions);
|
MediaFormat format = createFormat(bitRate, maxFps);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@@ -154,31 +151,14 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
private static MediaFormat createFormat(int bitRate, int maxFps) {
|
||||||
String key = codecOption.getKey();
|
|
||||||
Object value = codecOption.getValue();
|
|
||||||
|
|
||||||
if (value instanceof Integer) {
|
|
||||||
format.setInteger(key, (Integer) value);
|
|
||||||
} else if (value instanceof Long) {
|
|
||||||
format.setLong(key, (Long) value);
|
|
||||||
} else if (value instanceof Float) {
|
|
||||||
format.setFloat(key, (Float) value);
|
|
||||||
} else if (value instanceof String) {
|
|
||||||
format.setString(key, (String) value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval, List<CodecOption> codecOptions) {
|
|
||||||
MediaFormat format = new MediaFormat();
|
MediaFormat format = new MediaFormat();
|
||||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
|
||||||
// display the very first frame, and recover from bad quality when no new frames
|
// display the very first frame, and recover from bad quality when no new frames
|
||||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||||
if (maxFps > 0) {
|
if (maxFps > 0) {
|
||||||
@@ -187,13 +167,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codecOptions != null) {
|
|
||||||
for (CodecOption option : codecOptions) {
|
|
||||||
setCodecOption(format, option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.os.BatteryManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
@@ -20,7 +19,6 @@ public final class Server {
|
|||||||
private static void scrcpy(Options options) throws IOException {
|
private static void scrcpy(Options options) throws IOException {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
|
||||||
|
|
||||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||||
int restoreStayOn = -1;
|
int restoreStayOn = -1;
|
||||||
@@ -51,9 +49,8 @@ public final class Server {
|
|||||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
||||||
|
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
Controller controller = new Controller(device, connection);
|
||||||
@@ -112,55 +109,73 @@ public final class Server {
|
|||||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int expectedParameters = 13;
|
|
||||||
if (args.length != expectedParameters) {
|
|
||||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
for (int i = 1; i < args.length; ++i) {
|
||||||
options.setMaxSize(maxSize);
|
String arg = args[i];
|
||||||
|
int equalIndex = arg.indexOf('=');
|
||||||
|
if (equalIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
|
||||||
|
}
|
||||||
|
String key = arg.substring(0, equalIndex);
|
||||||
|
String value = arg.substring(equalIndex + 1);
|
||||||
|
|
||||||
int bitRate = Integer.parseInt(args[2]);
|
switch (key) {
|
||||||
options.setBitRate(bitRate);
|
case "max_size":
|
||||||
|
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||||
int maxFps = Integer.parseInt(args[3]);
|
options.setMaxSize(maxSize);
|
||||||
options.setMaxFps(maxFps);
|
break;
|
||||||
|
case "bit_rate":
|
||||||
int lockedVideoOrientation = Integer.parseInt(args[4]);
|
int bitRate = Integer.parseInt(value);
|
||||||
options.setLockedVideoOrientation(lockedVideoOrientation);
|
options.setBitRate(bitRate);
|
||||||
|
break;
|
||||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
case "max_fps":
|
||||||
boolean tunnelForward = Boolean.parseBoolean(args[5]);
|
int maxFps = Integer.parseInt(value);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setMaxFps(maxFps);
|
||||||
|
break;
|
||||||
Rect crop = parseCrop(args[6]);
|
case "lock_video_orientation":
|
||||||
options.setCrop(crop);
|
int lockedVideoOrientation = Integer.parseInt(value);
|
||||||
|
options.setLockedVideoOrientation(lockedVideoOrientation);
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
|
break;
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
case "tunnel_forward":
|
||||||
|
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||||
boolean control = Boolean.parseBoolean(args[8]);
|
boolean tunnelForward = Boolean.parseBoolean(value);
|
||||||
options.setControl(control);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
break;
|
||||||
int displayId = Integer.parseInt(args[9]);
|
case "crop":
|
||||||
options.setDisplayId(displayId);
|
Rect crop = parseCrop(value);
|
||||||
|
options.setCrop(crop);
|
||||||
boolean showTouches = Boolean.parseBoolean(args[10]);
|
break;
|
||||||
options.setShowTouches(showTouches);
|
case "send_frame_meta":
|
||||||
|
boolean sendFrameMeta = Boolean.parseBoolean(value);
|
||||||
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
options.setStayAwake(stayAwake);
|
break;
|
||||||
|
case "control":
|
||||||
String codecOptions = args[12];
|
boolean control = Boolean.parseBoolean(value);
|
||||||
options.setCodecOptions(codecOptions);
|
options.setControl(control);
|
||||||
|
break;
|
||||||
|
case "display_id":
|
||||||
|
int displayId = Integer.parseInt(value);
|
||||||
|
options.setDisplayId(displayId);
|
||||||
|
break;
|
||||||
|
case "show_touches":
|
||||||
|
boolean showTouches = Boolean.parseBoolean(value);
|
||||||
|
options.setShowTouches(showTouches);
|
||||||
|
break;
|
||||||
|
case "stay_awake":
|
||||||
|
boolean stayAwake = Boolean.parseBoolean(value);
|
||||||
|
options.setStayAwake(stayAwake);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown parameter: " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Rect parseCrop(String crop) {
|
private static Rect parseCrop(String crop) {
|
||||||
if ("-".equals(crop)) {
|
if (crop.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// input format: "width:height:x:y"
|
// input format: "width:height:x:y"
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
public class CodecOptionsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIntegerImplicit() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertEquals(5, option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInteger() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
|
||||||
Assert.assertEquals(5, option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLong() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Long);
|
|
||||||
Assert.assertEquals(5L, option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFloat() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Float);
|
|
||||||
Assert.assertEquals(4.5f, option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testString() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof String);
|
|
||||||
Assert.assertEquals("some_value", option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testStringEscaped() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
|
|
||||||
|
|
||||||
Assert.assertEquals(1, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("some_key", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof String);
|
|
||||||
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testList() {
|
|
||||||
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
|
|
||||||
|
|
||||||
Assert.assertEquals(5, codecOptions.size());
|
|
||||||
|
|
||||||
CodecOption option;
|
|
||||||
|
|
||||||
option = codecOptions.get(0);
|
|
||||||
Assert.assertEquals("a", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
|
||||||
Assert.assertEquals(1, option.getValue());
|
|
||||||
|
|
||||||
option = codecOptions.get(1);
|
|
||||||
Assert.assertEquals("b", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Integer);
|
|
||||||
Assert.assertEquals(2, option.getValue());
|
|
||||||
|
|
||||||
option = codecOptions.get(2);
|
|
||||||
Assert.assertEquals("c", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Long);
|
|
||||||
Assert.assertEquals(3L, option.getValue());
|
|
||||||
|
|
||||||
option = codecOptions.get(3);
|
|
||||||
Assert.assertEquals("d", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof Float);
|
|
||||||
Assert.assertEquals(4.5f, option.getValue());
|
|
||||||
|
|
||||||
option = codecOptions.get(4);
|
|
||||||
Assert.assertEquals("e", option.getKey());
|
|
||||||
Assert.assertTrue(option.getValue() instanceof String);
|
|
||||||
Assert.assertEquals("a,b=c", option.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user