diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java b/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java index 0b6ae152d5..84b273efc2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOptions.java @@ -1,10 +1,46 @@ package com.genymobile.scrcpy; +import android.media.MediaCodecInfo; +import android.os.Build; + import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; public class CodecOptions { - static final String PROFILE_OPTION = "profile"; - static final String LEVEL_OPTION = "level"; + + public static final String PROFILE_OPTION = "profile"; + public static final String LEVEL_OPTION = "level"; + + private static final LinkedHashMap levelsTable = new LinkedHashMap() { + { + // Adding all possible level and their properties + // 3rd property, bitrate was added but not sure if needed for now. + // Source: https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels + put(MediaCodecInfo.CodecProfileLevel.AVCLevel1, "485,99,64"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel1b, "485,99,128"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel11, "3000,396,192"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel12, "6000,396,384"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel13, "11880,396,768"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel2, "11880,396,2000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel21, "19800,792,4000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel22, "20250,1620,4000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel3, "40500,1620,10000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel31, "108000,3600,14000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel32, "216000,5120,20000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel4, "245760,8192,20000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel41, "245760,8192,50000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel42, "522240,8704,50000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel5, "589824,22080,135000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel51, "983040,36864,240000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel52, "2073600,36864,240000"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaCodecInfo.CodecProfileLevel.AVCLevel6, "4177920,139264,240000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel61, "8355840,139264,480000"); + put(MediaCodecInfo.CodecProfileLevel.AVCLevel62, "16711680,139264,800000"); + } + } + }; private HashMap options; @@ -12,7 +48,31 @@ public class CodecOptions { this.options = options; } - Object parseValue(String profileOption) { + /** + * The purpose of this function is to return the lowest possible codec profile level + * that supports the given width/height/bitrate of the stream + * @param width of the device + * @param height of the device + * @param bitRate at which we stream + * @return the lowest possible level that should support the given properties. + */ + public static int calculateLevel(int width, int height, int bitRate) { + // Calculations source: https://stackoverflow.com/questions/32100635/vlc-2-2-and-levels + int macroblocks = (int)( Math.ceil(width/16.0) * Math.ceil(height/16.0) ); + int macroblocks_s = macroblocks * 60; + for (Map.Entry entry : levelsTable.entrySet()) { + String[] levelProperties = entry.getValue().split(","); + int levelMacroblocks_s = Integer.parseInt(levelProperties[0]); + int levelMacroblocks = Integer.parseInt(levelProperties[1]); + if(macroblocks_s > levelMacroblocks_s) continue; + if(macroblocks > levelMacroblocks) continue; + Ln.i("Level selected based on screen size calculation: " + entry.getKey()); + return entry.getKey(); + } + return 0; + } + + public Object parseValue(String profileOption) { String value = options.get(profileOption); switch (profileOption) { case PROFILE_OPTION: diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 7f5aaf7f73..db8f2abda3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -68,13 +68,13 @@ public void streamScreen(Device device, FileDescriptor fd) throws IOException { try { do { MediaCodec codec = createCodec(); - setCodecProfile(codec, format); IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); Rect videoRect = screenInfo.getVideoSize().toRect(); int videoRotation = device.getVideoRotation(screenInfo.getRotation()); setSize(format, videoRotation, videoRect.width(), videoRect.height()); + setCodecProfile(codec, format, videoRect); configure(codec, format); Surface surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, videoRect); @@ -144,25 +144,42 @@ private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, IO.writeFully(fd, headerBuffer); } - private void setCodecProfile(MediaCodec codec, MediaFormat format) { + private void setCodecProfile(MediaCodec codec, MediaFormat format, Rect videoRect) { int profile = (int)codecOptions.parseValue(CodecOptions.PROFILE_OPTION); int level = (int)codecOptions.parseValue(CodecOptions.LEVEL_OPTION); + int suggestedLevel = CodecOptions.calculateLevel(videoRect.width(), videoRect.height(), bitRate); + boolean profileSupported = false; + if(profile == 0) return; - for (MediaCodecInfo.CodecProfileLevel profileLevel : codec.getCodecInfo().getCapabilitiesForType(MIMETYPE_VIDEO_AVC).profileLevels) { + for (MediaCodecInfo.CodecProfileLevel profileLevel : + codec.getCodecInfo().getCapabilitiesForType(MIMETYPE_VIDEO_AVC).profileLevels) { if(profileLevel.profile == profile) { - level = Math.max(level, profileLevel.level); + profileSupported = true; + break; } } - if(level == 0) { - Ln.w("Device doesn't support the requested codec profile.\n" + - "Profile and level will be chosen automatically."); - } else { - // Profile (SDK Level 21) and Level (SDK Level 23). + if(profileSupported) { + // Profile (SDK Level 21). format.setInteger(MediaFormat.KEY_PROFILE, profile); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - format.setInteger(MediaFormat.KEY_LEVEL, level); + if(level != 0) { + if(suggestedLevel != 0 && level != suggestedLevel) + Ln.w("Requested codec profile level is different from the suggested level"); + } else { + level = suggestedLevel; // If no level was given, use the pre calculated level. + } + // Level (SDK Level 23). + // We ask again because suggested level can be 0 and we cant set the level to 0, + // in that case we let the encoder choose the level. + if (level != 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + format.setInteger(MediaFormat.KEY_LEVEL, level); + } } + } else { + Ln.w("Device doesn't support the requested codec profile.\n" + + "Profile and level will be chosen automatically."); } + } private static MediaCodec createCodec() throws IOException {