Compare commits
1 Commits
copypaste
...
codec_opti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad3632f1ff |
@@ -210,6 +210,7 @@ To disable mirroring while recording:
|
|||||||
scrcpy --no-display --record file.mp4
|
scrcpy --no-display --record file.mp4
|
||||||
scrcpy -Nr file.mkv
|
scrcpy -Nr file.mkv
|
||||||
# interrupt recording with Ctrl+C
|
# interrupt recording with Ctrl+C
|
||||||
|
# Ctrl+C does not terminate properly on Windows, so disconnect the device
|
||||||
```
|
```
|
||||||
|
|
||||||
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ 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,6 +30,15 @@ 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"
|
||||||
@@ -472,12 +481,14 @@ 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'},
|
||||||
@@ -647,6 +658,9 @@ 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;
|
||||||
|
|||||||
@@ -7,10 +7,6 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
# include <windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@@ -49,18 +45,6 @@ static struct input_manager input_manager = {
|
|||||||
.prefer_text = false, // initialized later
|
.prefer_text = false, // initialized later
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
|
|
||||||
if (ctrl_type == CTRL_C_EVENT) {
|
|
||||||
SDL_Event event;
|
|
||||||
event.type = SDL_QUIT;
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
#endif // _WIN32
|
|
||||||
|
|
||||||
// init SDL and set appropriate hints
|
// init SDL and set appropriate hints
|
||||||
static bool
|
static bool
|
||||||
sdl_init_and_configure(bool display, const char *render_driver) {
|
sdl_init_and_configure(bool display, const char *render_driver) {
|
||||||
@@ -72,14 +56,6 @@ sdl_init_and_configure(bool display, const char *render_driver) {
|
|||||||
|
|
||||||
atexit(SDL_Quit);
|
atexit(SDL_Quit);
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
// Clean up properly on Ctrl+C on Windows
|
|
||||||
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
|
|
||||||
if (!ok) {
|
|
||||||
LOGW("Could not set Ctrl+C handler");
|
|
||||||
}
|
|
||||||
#endif // _WIN32
|
|
||||||
|
|
||||||
if (!display) {
|
if (!display) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -133,10 +109,10 @@ static int
|
|||||||
event_watcher(void *data, SDL_Event *event) {
|
event_watcher(void *data, SDL_Event *event) {
|
||||||
(void) data;
|
(void) data;
|
||||||
if (event->type == SDL_WINDOWEVENT
|
if (event->type == SDL_WINDOWEVENT
|
||||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
&& event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
// In practice, it seems to always be called from the same thread in
|
// In practice, it seems to always be called from the same thread in
|
||||||
// that specific case. Anyway, it's just a workaround.
|
// that specific case. Anyway, it's just a workaround.
|
||||||
screen_render(&screen);
|
screen_handle_window_event(&screen, &event->window);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -303,6 +279,7 @@ 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,6 +16,7 @@ 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;
|
||||||
@@ -48,6 +49,7 @@ 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, \
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
display_id_string,
|
display_id_string,
|
||||||
params->show_touches ? "true" : "false",
|
params->show_touches ? "true" : "false",
|
||||||
params->stay_awake ? "true" : "false",
|
params->stay_awake ? "true" : "false",
|
||||||
|
params->codec_options ? params->codec_options : "-",
|
||||||
};
|
};
|
||||||
#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 "
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2008, The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package android.content;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@hide}
|
|
||||||
*/
|
|
||||||
oneway interface IOnPrimaryClipChangedListener {
|
|
||||||
void dispatchPrimaryClipChanged();
|
|
||||||
}
|
|
||||||
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -215,7 +215,7 @@ public class Controller {
|
|||||||
|
|
||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_MOUSE, 0);
|
||||||
return injectEvent(event);
|
return injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
import android.content.IOnPrimaryClipChangedListener;
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
@@ -67,14 +66,6 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
}, displayId);
|
}, displayId);
|
||||||
|
|
||||||
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
|
||||||
@Override
|
|
||||||
public void dispatchPrimaryClipChanged() {
|
|
||||||
String s = getClipboardText();
|
|
||||||
Ln.i("clipboard changed = " + s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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;
|
||||||
@@ -102,4 +103,12 @@ 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,6 +12,7 @@ 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 {
|
||||||
@@ -25,15 +26,17 @@ 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) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -61,7 +64,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);
|
MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL, codecOptions);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@@ -151,14 +154,31 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(int bitRate, int maxFps) {
|
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||||
|
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, DEFAULT_I_FRAME_INTERVAL);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
||||||
// 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) {
|
||||||
@@ -167,11 +187,18 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IBinder createDisplay() {
|
private static IBinder createDisplay() {
|
||||||
return SurfaceControl.createDisplay("scrcpy", false);
|
return SurfaceControl.createDisplay("scrcpy", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
private static void configure(MediaCodec codec, MediaFormat format) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 {
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ 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;
|
||||||
@@ -49,8 +51,9 @@ 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());
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
Controller controller = new Controller(device, connection);
|
||||||
@@ -109,7 +112,7 @@ 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 = 12;
|
final int expectedParameters = 13;
|
||||||
if (args.length != expectedParameters) {
|
if (args.length != expectedParameters) {
|
||||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
||||||
}
|
}
|
||||||
@@ -150,6 +153,9 @@ public final class Server {
|
|||||||
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
||||||
options.setStayAwake(stayAwake);
|
options.setStayAwake(stayAwake);
|
||||||
|
|
||||||
|
String codecOptions = args[12];
|
||||||
|
options.setCodecOptions(codecOptions);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
|
|||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.IOnPrimaryClipChangedListener;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ public class ClipboardManager {
|
|||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getPrimaryClipMethod;
|
private Method getPrimaryClipMethod;
|
||||||
private Method setPrimaryClipMethod;
|
private Method setPrimaryClipMethod;
|
||||||
private Method addPrimaryClipChangedListener;
|
|
||||||
|
|
||||||
public ClipboardManager(IInterface manager) {
|
public ClipboardManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -83,37 +81,4 @@ public class ClipboardManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
|
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
|
||||||
} else {
|
|
||||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
|
||||||
if (addPrimaryClipChangedListener == null) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
addPrimaryClipChangedListener = manager.getClass()
|
|
||||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
|
||||||
} else {
|
|
||||||
addPrimaryClipChangedListener = manager.getClass()
|
|
||||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addPrimaryClipChangedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
|
||||||
try {
|
|
||||||
Method method = getAddPrimaryClipChangedListener();
|
|
||||||
addPrimaryClipChangedListener(method, manager, listener);
|
|
||||||
return true;
|
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not invoke method", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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