mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-12-17 05:24:19 +01:00
There are a lot of "magic numbers" that we really don't want to extract as a constant. Until now, many @SuppressWarnings annotations were added, but it makes no sense to check for magic number if we silent the warnings everywhere.
195 lines
7.5 KiB
Java
195 lines
7.5 KiB
Java
package com.genymobile.scrcpy;
|
|
|
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
|
|
|
import android.graphics.Rect;
|
|
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_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";
|
|
|
|
private static final int NO_PTS = -1;
|
|
|
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
|
|
|
private int bitRate;
|
|
private int maxFps;
|
|
private int lockedVideoOrientation;
|
|
private int iFrameInterval;
|
|
private boolean sendFrameMeta;
|
|
private long ptsOrigin;
|
|
|
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) {
|
|
this.sendFrameMeta = sendFrameMeta;
|
|
this.bitRate = bitRate;
|
|
this.maxFps = maxFps;
|
|
this.lockedVideoOrientation = lockedVideoOrientation;
|
|
this.iFrameInterval = iFrameInterval;
|
|
}
|
|
|
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) {
|
|
this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, 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 {
|
|
Workarounds.prepareMainLooper();
|
|
Workarounds.fillAppInfo();
|
|
|
|
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
|
device.setRotationListener(this);
|
|
boolean alive;
|
|
try {
|
|
do {
|
|
MediaCodec codec = createCodec();
|
|
IBinder display = createDisplay();
|
|
ScreenInfo screenInfo = device.getScreenInfo();
|
|
Rect contentRect = screenInfo.getContentRect();
|
|
// include the locked video orientation
|
|
Rect videoRect = screenInfo.getVideoSize().toRect();
|
|
// does not include the locked video orientation
|
|
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
|
int videoRotation = screenInfo.getVideoRotation();
|
|
setSize(format, videoRect.width(), videoRect.height());
|
|
configure(codec, format);
|
|
Surface surface = codec.createInputSurface();
|
|
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect);
|
|
codec.start();
|
|
try {
|
|
alive = encode(codec, fd);
|
|
// do not call stop() on exception, it would trigger an IllegalStateException
|
|
codec.stop();
|
|
} finally {
|
|
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();
|
|
|
|
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);
|
|
|
|
if (sendFrameMeta) {
|
|
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
|
}
|
|
|
|
IO.writeFully(fd, codecBuffer);
|
|
}
|
|
} finally {
|
|
if (outputBufferId >= 0) {
|
|
codec.releaseOutputBuffer(outputBufferId, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
return !eof;
|
|
}
|
|
|
|
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
|
headerBuffer.clear();
|
|
|
|
long pts;
|
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
pts = NO_PTS; // non-media data packet
|
|
} else {
|
|
if (ptsOrigin == 0) {
|
|
ptsOrigin = bufferInfo.presentationTimeUs;
|
|
}
|
|
pts = bufferInfo.presentationTimeUs - ptsOrigin;
|
|
}
|
|
|
|
headerBuffer.putLong(pts);
|
|
headerBuffer.putInt(packetSize);
|
|
headerBuffer.flip();
|
|
IO.writeFully(fd, headerBuffer);
|
|
}
|
|
|
|
private static MediaCodec createCodec() throws IOException {
|
|
return MediaCodec.createEncoderByType("video/avc");
|
|
}
|
|
|
|
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
|
|
MediaFormat format = new MediaFormat();
|
|
format.setString(MediaFormat.KEY_MIME, "video/avc");
|
|
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
|
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
|
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, REPEAT_FRAME_DELAY_US); // µs
|
|
if (maxFps > 0) {
|
|
// The key existed privately before Android 10:
|
|
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>
|
|
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
|
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
|
}
|
|
return format;
|
|
}
|
|
|
|
private static IBinder createDisplay() {
|
|
return SurfaceControl.createDisplay("scrcpy", true);
|
|
}
|
|
|
|
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, int orientation, Rect deviceRect, Rect displayRect) {
|
|
SurfaceControl.openTransaction();
|
|
try {
|
|
SurfaceControl.setDisplaySurface(display, surface);
|
|
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
|
|
SurfaceControl.setDisplayLayerStack(display, 0);
|
|
} finally {
|
|
SurfaceControl.closeTransaction();
|
|
}
|
|
}
|
|
|
|
private static void destroyDisplay(IBinder display) {
|
|
SurfaceControl.destroyDisplay(display);
|
|
}
|
|
}
|