You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
scrcpy/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java

158 lines
5.8 KiB
Java

package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.media.MediaMuxer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.IBinder;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_FRAME_RATE = 60; // fps
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private int bitRate;
private int frameRate;
private int iFrameInterval;
public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) {
this.bitRate = bitRate;
this.frameRate = frameRate;
this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(int bitRate) {
this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
}
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
device.setRotationListener(this);
boolean alive;
try {
do {
MediaCodec codec = createCodec();
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
codec.start();
try {
alive = encode(codec, fd);
} finally {
codec.stop();
destroyDisplay(display);
codec.release();
surface.release();
}
} while (alive);
} finally {
device.setRotationListener(null);
}
}
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer bBuffer = ByteBuffer.allocate(16);
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
bBuffer.position(0);
bBuffer.putLong(bufferInfo.presentationTimeUs);
bBuffer.putInt(bufferInfo.flags);
bBuffer.putInt(codecBuffer.remaining());
bBuffer.position(0);
IO.writeFully(fd, bBuffer);
IO.writeFully(fd, codecBuffer);
}
} finally {
if (outputBufferId >= 0) {
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}
return !eof;
}
private static MediaCodec createCodec() throws IOException {
return MediaCodec.createEncoderByType("video/avc");
}
private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "video/avc");
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs
return format;
}
private static IBinder createDisplay() {
return SurfaceControl.createDisplay("scrcpy", false);
}
private static void configure(MediaCodec codec, MediaFormat format) {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
private static void setSize(MediaFormat format, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
SurfaceControl.openTransaction();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, 0);
} finally {
SurfaceControl.closeTransaction();
}
}
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
}