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.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; 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 String encoderName; private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; } @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(); try { internalStreamScreen(device, fd); } catch (NullPointerException e) { // Retry with workarounds enabled: // // Ln.d("Applying workarounds to avoid NullPointerException"); Workarounds.fillAppInfo(); internalStreamScreen(device, fd); } } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(encoderName); 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(); int layerStack = device.getLayerStack(); setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); 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 MediaCodecInfo[] listEncoders() { List result = new ArrayList<>(); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo codecInfo : list.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { result.add(codecInfo); } } return result.toArray(new MediaCodecInfo[result.size()]); } private static MediaCodec createCodec(String encoderName) throws IOException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { MediaCodecInfo[] encoders = listEncoders(); throw new InvalidEncoderException(encoderName, encoders); } } MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); Ln.d("Using encoder: '" + codec.getName() + "'"); return codec; } 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, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_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, DEFAULT_I_FRAME_INTERVAL); // 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: // // format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); } if (codecOptions != null) { for (CodecOption option : codecOptions) { setCodecOption(format, option); } } return format; } private static IBinder createDisplay() { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" .equals(Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } 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, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } } private static void destroyDisplay(IBinder display) { SurfaceControl.destroyDisplay(display); } }