Avoid additional buffer copy in userspace

Directly send the data from MediaCodec buffers to the LocalSocket,
without an intermediate copy in userspace.
This commit is contained in:
Romain Vimont 2018-09-09 15:01:55 +02:00
parent a60aef5aaf
commit 66def38b73
4 changed files with 44 additions and 22 deletions

View file

@ -5,9 +5,9 @@ import android.net.LocalSocket;
import android.net.LocalSocketAddress; import android.net.LocalSocketAddress;
import java.io.Closeable; import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable { public final class DesktopConnection implements Closeable {
@ -18,14 +18,14 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket socket; private final LocalSocket socket;
private final InputStream inputStream; private final InputStream inputStream;
private final OutputStream outputStream; private final FileDescriptor fd;
private final ControlEventReader reader = new ControlEventReader(); private final ControlEventReader reader = new ControlEventReader();
private DesktopConnection(LocalSocket socket) throws IOException { private DesktopConnection(LocalSocket socket) throws IOException {
this.socket = socket; this.socket = socket;
inputStream = socket.getInputStream(); inputStream = socket.getInputStream();
outputStream = socket.getOutputStream(); fd = socket.getFileDescriptor();
} }
private static LocalSocket connect(String abstractName) throws IOException { private static LocalSocket connect(String abstractName) throws IOException {
@ -78,11 +78,11 @@ public final class DesktopConnection implements Closeable {
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
outputStream.write(buffer, 0, buffer.length); IO.writeFully(fd, buffer, 0, buffer.length);
} }
public OutputStream getOutputStream() { public FileDescriptor getFd() {
return outputStream; return fd;
} }
public ControlEvent receiveControlEvent() throws IOException { public ControlEvent receiveControlEvent() throws IOException {

View file

@ -0,0 +1,31 @@
package com.genymobile.scrcpy;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public class IO {
private IO() {
// not instantiable
}
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
while (from.hasRemaining()) {
try {
Os.write(fd, from);
} catch (ErrnoException e) {
if (e.errno != OsConstants.EINTR) {
throw new IOException(e);
}
}
}
}
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
}
}

View file

@ -9,8 +9,8 @@ import android.media.MediaFormat;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -48,7 +48,7 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false); return rotationChanged.getAndSet(false);
} }
public void streamScreen(Device device, OutputStream outputStream) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
@ -64,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener {
setDisplaySurface(display, surface, contentRect, videoRect); setDisplaySurface(display, surface, contentRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, outputStream); alive = encode(codec, fd);
} finally { } finally {
codec.stop(); codec.stop();
destroyDisplay(display); destroyDisplay(display);
@ -77,9 +77,7 @@ public class ScreenEncoder implements Device.RotationListener {
} }
} }
private boolean encode(MediaCodec codec, OutputStream outputStream) throws IOException { private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
@SuppressWarnings("checkstyle:MagicNumber")
byte[] buf = new byte[bitRate / 8]; // may contain up to 1 second of video
boolean eof = false; boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) { while (!consumeRotationChange() && !eof) {
@ -91,15 +89,8 @@ public class ScreenEncoder implements Device.RotationListener {
break; break;
} }
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
while (outputBuffer.hasRemaining()) { IO.writeFully(fd, codecBuffer);
int remaining = outputBuffer.remaining();
int len = Math.min(buf.length, remaining);
// the outputBuffer is probably direct (it has no underlying array), and LocalSocket does not expose channels,
// so we must copy the data locally to write them manually to the output stream
outputBuffer.get(buf, 0, len);
outputStream.write(buf, 0, len);
}
} }
} finally { } finally {
if (outputBufferId >= 0) { if (outputBufferId >= 0) {

View file

@ -21,7 +21,7 @@ public final class Server {
try { try {
// synchronous // synchronous
screenEncoder.streamScreen(device, connection.getOutputStream()); screenEncoder.streamScreen(device, connection.getFd());
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");