Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
422f15c595 | ||
|
|
7ebaf5fe87 | ||
|
|
de95b61f81 | ||
|
|
18082bd7ab | ||
|
|
9654e0920b | ||
|
|
3f69e73722 | ||
|
|
eb1830449e | ||
|
|
56df8cd48f | ||
|
|
6c406c9319 | ||
|
|
44e697cd9a | ||
|
|
132be1a81b | ||
|
|
5138ce75aa | ||
|
|
ff3c3670b2 | ||
|
|
e50e409405 | ||
|
|
07b65038a1 |
31
README.md
31
README.md
@@ -194,6 +194,18 @@ The other dimension is computed so that the Android device aspect ratio is
|
||||
preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
||||
|
||||
|
||||
#### Select codec
|
||||
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```bash
|
||||
scrcpy --codec=h264 # default
|
||||
scrcpy --codec=h265
|
||||
scrcpy --codec=av1
|
||||
```
|
||||
|
||||
|
||||
#### Change bit-rate
|
||||
|
||||
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
||||
@@ -252,19 +264,7 @@ This affects recording orientation.
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### Codec
|
||||
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```bash
|
||||
scrcpy --codec=h264 # default
|
||||
scrcpy --codec=h265
|
||||
scrcpy --codec=av1
|
||||
```
|
||||
|
||||
|
||||
##### Encoder
|
||||
#### Encoder
|
||||
|
||||
Some devices have more than one encoder for a specific codec, and some of them
|
||||
may cause issues or crash. It is possible to select a different encoder:
|
||||
@@ -277,10 +277,11 @@ To list the available encoders, you can pass an invalid encoder name; the
|
||||
error will give the available encoders:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=_ # for the default codec
|
||||
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||
scrcpy --encoder=_
|
||||
```
|
||||
|
||||
Note that you can also select a different [codec](#select-codec).
|
||||
|
||||
### Capture
|
||||
|
||||
#### Recording
|
||||
|
||||
@@ -272,8 +272,6 @@ end:
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(socket != SC_SOCKET_NONE);
|
||||
|
||||
demuxer->socket = socket;
|
||||
demuxer->pending = NULL;
|
||||
demuxer->sink_count = 0;
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
struct scrcpy {
|
||||
struct sc_server server;
|
||||
struct sc_screen screen;
|
||||
struct sc_demuxer video_demuxer;
|
||||
struct sc_demuxer audio_demuxer;
|
||||
struct sc_demuxer demuxer;
|
||||
struct sc_decoder decoder;
|
||||
struct sc_recorder recorder;
|
||||
#ifdef HAVE_V4L2
|
||||
@@ -234,21 +233,13 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
}
|
||||
|
||||
static void
|
||||
sc_video_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
static void
|
||||
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
@@ -304,8 +295,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
#ifdef HAVE_V4L2
|
||||
bool v4l2_sink_initialized = false;
|
||||
#endif
|
||||
bool video_demuxer_started = false;
|
||||
bool audio_demuxer_started = false;
|
||||
bool demuxer_started = false;
|
||||
#ifdef HAVE_USB
|
||||
bool aoa_hid_initialized = false;
|
||||
bool hid_keyboard_initialized = false;
|
||||
@@ -431,26 +421,17 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||
.on_eos = sc_video_demuxer_on_eos,
|
||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
||||
.on_eos = sc_demuxer_on_eos,
|
||||
};
|
||||
sc_demuxer_init(&s->video_demuxer, s->server.video_socket,
|
||||
&video_demuxer_cbs, NULL);
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||
.on_eos = sc_audio_demuxer_on_eos,
|
||||
};
|
||||
sc_demuxer_init(&s->audio_demuxer, s->server.audio_socket,
|
||||
&audio_demuxer_cbs, NULL);
|
||||
}
|
||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
||||
|
||||
if (dec) {
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &dec->packet_sink);
|
||||
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
|
||||
}
|
||||
|
||||
if (rec) {
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &rec->packet_sink);
|
||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
||||
}
|
||||
|
||||
struct sc_controller *controller = NULL;
|
||||
@@ -658,24 +639,17 @@ aoa_hid_end:
|
||||
#endif
|
||||
|
||||
// now we consumed the header values, the socket receives the video stream
|
||||
// start the video demuxer
|
||||
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||
// start the demuxer
|
||||
if (!sc_demuxer_start(&s->demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
video_demuxer_started = true;
|
||||
|
||||
if (options->audio) {
|
||||
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
audio_demuxer_started = true;
|
||||
}
|
||||
demuxer_started = true;
|
||||
|
||||
ret = event_loop(s);
|
||||
LOGD("quit...");
|
||||
|
||||
// Close the window immediately on closing, because screen_destroy() may
|
||||
// only be called once the video demuxer thread is joined (it may take time)
|
||||
// only be called once the demuxer thread is joined (it may take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
|
||||
end:
|
||||
@@ -713,12 +687,8 @@ end:
|
||||
|
||||
// now that the sockets are shutdown, the demuxer and controller are
|
||||
// interrupted, we can join them
|
||||
if (video_demuxer_started) {
|
||||
sc_demuxer_join(&s->video_demuxer);
|
||||
}
|
||||
|
||||
if (audio_demuxer_started) {
|
||||
sc_demuxer_join(&s->audio_demuxer);
|
||||
if (demuxer_started) {
|
||||
sc_demuxer_join(&s->demuxer);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
@@ -737,9 +707,8 @@ end:
|
||||
}
|
||||
#endif
|
||||
|
||||
// Destroy the screen only after the video demuxer is guaranteed to be
|
||||
// finished, because otherwise the screen could receive new frames after
|
||||
// destruction
|
||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
||||
// because otherwise the screen could receive new frames after destruction
|
||||
if (screen_initialized) {
|
||||
sc_screen_join(&s->screen);
|
||||
sc_screen_destroy(&s->screen);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum AudioCodec implements Codec {
|
||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
private final String name;
|
||||
private final String mimeType;
|
||||
|
||||
AudioCodec(int id, String name, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.VIDEO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static AudioCodec findByName(String name) {
|
||||
for (AudioCodec codec : values()) {
|
||||
if (codec.name.equals(name)) {
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.AudioTimestamp;
|
||||
@@ -15,30 +19,11 @@ import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class AudioEncoder {
|
||||
|
||||
private static class InputTask {
|
||||
final int index;
|
||||
|
||||
InputTask(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
private static class OutputTask {
|
||||
final int index;
|
||||
final MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
this.index = index;
|
||||
this.bufferInfo = bufferInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS;
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int CHANNELS = 2;
|
||||
@@ -49,21 +34,9 @@ public final class AudioEncoder {
|
||||
|
||||
private AudioRecord recorder;
|
||||
private MediaCodec mediaCodec;
|
||||
|
||||
private final AtomicBoolean cleanUpDone = new AtomicBoolean(false);
|
||||
|
||||
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
|
||||
|
||||
private Thread thread;
|
||||
private HandlerThread mediaCodecThread;
|
||||
|
||||
private Thread inputThread;
|
||||
private Thread outputThread;
|
||||
|
||||
private boolean ended;
|
||||
private HandlerThread thread;
|
||||
private final AtomicBoolean interrupted = new AtomicBoolean();
|
||||
private final Semaphore endSemaphore = new Semaphore(0); // blocks until encoding is ended
|
||||
|
||||
private static AudioFormat createAudioFormat() {
|
||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||
@@ -96,207 +69,139 @@ public final class AudioEncoder {
|
||||
return format;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
private void inputThread() throws IOException, InterruptedException {
|
||||
final AudioTimestamp timestamp = new AudioTimestamp();
|
||||
long previousPts = 0;
|
||||
long nextPts = 0;
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
InputTask task = inputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||
int r = recorder.read(buffer, BUFFER_SIZE);
|
||||
if (r < 0) {
|
||||
throw new IOException("Could not read audio: " + r);
|
||||
}
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void start() throws IOException {
|
||||
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
|
||||
|
||||
long pts;
|
||||
recorder = createAudioRecord();
|
||||
|
||||
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
||||
if (ret == AudioRecord.SUCCESS) {
|
||||
pts = timestamp.nanoTime / 1000;
|
||||
} else {
|
||||
if (nextPts == 0) {
|
||||
Ln.w("Could not get any audio timestamp");
|
||||
}
|
||||
// compute from previous timestamp and packet size
|
||||
pts = nextPts;
|
||||
}
|
||||
MediaFormat format = createFormat();
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
|
||||
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||
nextPts = pts + durationMs;
|
||||
recorder.startRecording();
|
||||
|
||||
if (previousPts != 0 && pts < previousPts) {
|
||||
// Audio PTS may come from two sources:
|
||||
// - recorder.getTimestamp() if the call works;
|
||||
// - an estimation from the previous PTS and the packet size as a fallback.
|
||||
//
|
||||
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
||||
pts = previousPts + 1;
|
||||
}
|
||||
|
||||
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
|
||||
|
||||
previousPts = pts;
|
||||
}
|
||||
}
|
||||
|
||||
private void outputThread() throws IOException, InterruptedException {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
OutputTask task = outputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||
try {
|
||||
Ln.i("Audio packet [pts=" + task.bufferInfo.presentationTimeUs + "] " + buffer.remaining() + " bytes");
|
||||
} finally {
|
||||
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
encode();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Audio encoder stopped");
|
||||
}
|
||||
});
|
||||
thread = new HandlerThread("AudioEncoder");
|
||||
thread.start();
|
||||
|
||||
class AudioEncoderCallbacks extends MediaCodec.Callback {
|
||||
|
||||
private final AudioTimestamp timestamp = new AudioTimestamp();
|
||||
private long previousPts;
|
||||
private long nextPts;
|
||||
private boolean eofSignaled;
|
||||
private boolean ended;
|
||||
|
||||
private void notifyEnded() {
|
||||
assert !ended;
|
||||
ended = true;
|
||||
endSemaphore.release();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
if (eofSignaled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer inputBuffer = codec.getInputBuffer(index);
|
||||
int r = recorder.read(inputBuffer, BUFFER_SIZE);
|
||||
|
||||
long pts;
|
||||
|
||||
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
||||
if (ret == AudioRecord.SUCCESS) {
|
||||
pts = timestamp.nanoTime / 1000;
|
||||
} else {
|
||||
if (nextPts == 0) {
|
||||
Ln.w("Could not get any audio timestamp");
|
||||
}
|
||||
// compute from previous timestamp and packet size
|
||||
pts = nextPts;
|
||||
}
|
||||
|
||||
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||
nextPts = pts + durationMs;
|
||||
|
||||
int flags = 0;
|
||||
if (interrupted.get()) {
|
||||
flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||
eofSignaled = true;
|
||||
}
|
||||
|
||||
if (previousPts != 0 && pts < previousPts) {
|
||||
// Audio PTS may come from two sources:
|
||||
// - recorder.getTimestamp() if the call works;
|
||||
// - an estimation from the previous PTS and the packet size as a fallback.
|
||||
//
|
||||
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
||||
pts = previousPts + 1;
|
||||
}
|
||||
|
||||
codec.queueInputBuffer(index, 0, r, pts, flags);
|
||||
|
||||
previousPts = pts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
if (ended) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(index);
|
||||
try {
|
||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||
long pts = bufferInfo.presentationTimeUs;
|
||||
Ln.i("Audio packet: pts=" + pts + " " + codecBuffer.remaining() + " bytes");
|
||||
} finally {
|
||||
codec.releaseOutputBuffer(index, false);
|
||||
}
|
||||
|
||||
boolean eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
if (eof) {
|
||||
notifyEnded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||
Ln.e("MediaCodec error", e);
|
||||
if (!ended) {
|
||||
notifyEnded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
mediaCodec.setCallback(new AudioEncoderCallbacks(), new Handler(thread.getLooper()));
|
||||
mediaCodec.start();
|
||||
}
|
||||
|
||||
private void waitEnded() {
|
||||
try {
|
||||
endSemaphore.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Ln.i("==== STOP");
|
||||
if (thread != null) {
|
||||
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void end() {
|
||||
ended = true;
|
||||
notify();
|
||||
}
|
||||
|
||||
private synchronized void waitEnded() {
|
||||
try {
|
||||
while (!ended) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void encode() throws IOException {
|
||||
mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException
|
||||
|
||||
try {
|
||||
recorder = createAudioRecord();
|
||||
|
||||
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||
mediaCodecThread.start();
|
||||
|
||||
MediaFormat format = createFormat();
|
||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
|
||||
recorder.startRecording();
|
||||
|
||||
inputThread = new Thread(() -> {
|
||||
try {
|
||||
inputThread();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
});
|
||||
|
||||
outputThread = new Thread(() -> {
|
||||
try {
|
||||
outputThread();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
});
|
||||
|
||||
mediaCodec.start();
|
||||
inputThread.start();
|
||||
outputThread.start();
|
||||
} catch (Throwable e) {
|
||||
mediaCodec.release();
|
||||
if (recorder != null) {
|
||||
recorder.release();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
interrupted.set(true);
|
||||
waitEnded();
|
||||
} finally {
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanUp() {
|
||||
mediaCodecThread.getLooper().quit();
|
||||
inputThread.interrupt();
|
||||
outputThread.interrupt();
|
||||
|
||||
try {
|
||||
mediaCodecThread.join();
|
||||
inputThread.join();
|
||||
outputThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
// Should never happen
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
mediaCodec.stop();
|
||||
mediaCodec.release();
|
||||
recorder.stop();
|
||||
recorder.release();
|
||||
}
|
||||
|
||||
private class EncoderCallback extends MediaCodec.Callback {
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
try {
|
||||
inputTasks.put(new InputTask(index));
|
||||
} catch (InterruptedException e) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
try {
|
||||
outputTasks.put(new OutputTask(index, bufferInfo));
|
||||
} catch (InterruptedException e) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||
Ln.e("MediaCodec error", e);
|
||||
end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
// ignore
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
mediaCodec.stop();
|
||||
mediaCodec.release();
|
||||
recorder.stop();
|
||||
Ln.i("==== STOPPED");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
enum Type {VIDEO}
|
||||
|
||||
Type getType();
|
||||
int getId();
|
||||
String getName();
|
||||
String getMimeType();
|
||||
}
|
||||
@@ -90,7 +90,6 @@ public class Controller {
|
||||
control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
});
|
||||
@@ -101,17 +100,11 @@ public class Controller {
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
sender.stop();
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
sender.join();
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@@ -83,9 +83,6 @@ public final class DesktopConnection implements Closeable {
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(socketName);
|
||||
if (audio) {
|
||||
audioSocket = connect(socketName);
|
||||
}
|
||||
if (control) {
|
||||
controlSocket = connect(socketName);
|
||||
}
|
||||
|
||||
@@ -277,26 +277,6 @@ public final class Device {
|
||||
* @param mode one of the {@code POWER_MODE_*} constants
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Change the power mode for all physical displays
|
||||
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||
if (physicalDisplayIds == null) {
|
||||
Ln.e("Could not get physical display ids");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
if (!ok) {
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
||||
// Older Android versions, only 1 display
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
|
||||
@@ -57,7 +57,6 @@ public final class DeviceMessageSender {
|
||||
loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
@@ -67,12 +66,7 @@ public final class DeviceMessageSender {
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Rect;
|
||||
import java.util.List;
|
||||
|
||||
public class Options {
|
||||
private static final String VIDEO_CODEC_H264 = "h264";
|
||||
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int uid = -1; // 31-bit non-negative value, or -1
|
||||
|
||||
@@ -21,6 +21,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
public interface Callbacks {
|
||||
void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException;
|
||||
}
|
||||
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
||||
@@ -31,8 +35,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
|
||||
private final Device device;
|
||||
private final Streamer streamer;
|
||||
private final String videoMimeType;
|
||||
private final String encoderName;
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final int bitRate;
|
||||
@@ -42,10 +45,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List<CodecOption> codecOptions,
|
||||
String encoderName, boolean downsizeOnError) {
|
||||
this.device = device;
|
||||
this.streamer = streamer;
|
||||
public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||
this.videoMimeType = videoMimeType;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
@@ -62,15 +63,11 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public void streamScreen() throws IOException {
|
||||
String videoMimeType = streamer.getCodec().getMimeType();
|
||||
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
||||
MediaCodec codec = createCodec(videoMimeType, encoderName);
|
||||
MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
|
||||
streamer.writeHeader();
|
||||
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
@@ -95,7 +92,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
codec.start();
|
||||
|
||||
alive = encode(codec, streamer);
|
||||
alive = encode(codec, callbacks);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
codec.stop();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
@@ -164,7 +161,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
|
||||
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
@@ -187,7 +184,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
consecutiveErrors = 0;
|
||||
}
|
||||
|
||||
streamer.writePacket(codecBuffer, bufferInfo);
|
||||
callbacks.onPacket(codecBuffer, bufferInfo);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
|
||||
@@ -99,6 +99,8 @@ public final class Server {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
}
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName(), options.getDownsizeOnError());
|
||||
|
||||
Controller controller = null;
|
||||
if (control) {
|
||||
@@ -115,34 +117,23 @@ public final class Server {
|
||||
audioEncoder.start();
|
||||
}
|
||||
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(),
|
||||
codecOptions, options.getEncoderName(), options.getDownsizeOnError());
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen();
|
||||
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
||||
if (options.getSendCodecId()) {
|
||||
videoStreamer.writeHeader(codec.getId());
|
||||
}
|
||||
screenEncoder.streamScreen(device, videoStreamer);
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.stop();
|
||||
}
|
||||
if (controller != null) {
|
||||
controller.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
initThread.join();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.join();
|
||||
}
|
||||
if (controller != null) {
|
||||
controller.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum VideoCodec implements Codec {
|
||||
public enum VideoCodec {
|
||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||
@@ -17,22 +17,10 @@ public enum VideoCodec implements Codec {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.VIDEO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
@@ -6,39 +6,30 @@ import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class Streamer {
|
||||
public final class VideoStreamer implements ScreenEncoder.Callbacks {
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
|
||||
private final FileDescriptor fd;
|
||||
private final Codec codec;
|
||||
private final boolean sendCodecId;
|
||||
private final boolean sendFrameMeta;
|
||||
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
|
||||
this.fd = fd;
|
||||
this.codec = codec;
|
||||
this.sendCodecId = sendCodecId;
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public Codec getCodec() {
|
||||
return codec;
|
||||
public void writeHeader(int codecId) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codecId);
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
|
||||
public void writeHeader() throws IOException {
|
||||
if (sendCodecId) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
@Override
|
||||
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
@@ -30,8 +30,6 @@ public final class SurfaceControl {
|
||||
|
||||
private static Method getBuiltInDisplayMethod;
|
||||
private static Method setDisplayPowerModeMethod;
|
||||
private static Method getPhysicalDisplayTokenMethod;
|
||||
private static Method getPhysicalDisplayIdsMethod;
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
@@ -100,6 +98,7 @@ public final class SurfaceControl {
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@@ -115,40 +114,6 @@ public final class SurfaceControl {
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayTokenMethod == null) {
|
||||
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
||||
}
|
||||
return getPhysicalDisplayTokenMethod;
|
||||
}
|
||||
|
||||
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayTokenMethod();
|
||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayIdsMethod == null) {
|
||||
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
||||
}
|
||||
return getPhysicalDisplayIdsMethod;
|
||||
}
|
||||
|
||||
public static long[] getPhysicalDisplayIds() {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayIdsMethod();
|
||||
return (long[]) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||
if (setDisplayPowerModeMethod == null) {
|
||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||
|
||||
Reference in New Issue
Block a user