Add support for streaming H.265 from Maxwell 2 cards

This commit is contained in:
Cameron Gutman 2015-12-12 21:11:08 -08:00
parent 2c5e6c0788
commit 3f46485382
11 changed files with 258 additions and 85 deletions

Binary file not shown.

View File

@ -208,7 +208,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
decoderRenderer.initializeWithFlags(drFlags, prefConfig.videoFormat);
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
Toast.makeText(this, "No H.265 decoder found. Falling back to H.264", Toast.LENGTH_LONG).show();
}
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
@ -221,6 +226,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.setRemote(remote)
.setHevcSupported(decoderRenderer.isHevcSupported())
.setAudioConfiguration(prefConfig.enable51Surround ?
StreamConfiguration.AUDIO_CONFIGURATION_5_1 :
StreamConfiguration.AUDIO_CONFIGURATION_STEREO)
@ -352,6 +358,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
unbindService(usbDriverServiceConnection);
}
VideoDecoderRenderer.VideoFormat videoFormat = conn.getActiveVideoFormat();
displayedFailureDialog = true;
stopConnection();
@ -368,6 +376,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
}
// Add the video codec to the post-stream toast
if (message != null && videoFormat != VideoDecoderRenderer.VideoFormat.Unknown) {
if (videoFormat == VideoDecoderRenderer.VideoFormat.H265) {
message += " [H.265]";
}
else {
message += " [H.264]";
}
}
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}

View File

@ -91,9 +91,14 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
public boolean setup(VideoFormat format, int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
// We should never make it here with H265
if (format != VideoFormat.H264) {
return false;
}
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
@ -283,7 +288,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
}
@Override
public String getDecoderName() {
return "CPU decoding";
public boolean isHevcSupported() {
return false;
}
}

View File

@ -1,5 +1,7 @@
package com.limelight.binding.video;
import android.media.MediaCodecInfo;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
@ -16,18 +18,18 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
public boolean setup(VideoFormat format, int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
return decoderRenderer.setup(format, width, height, redrawRate, renderTarget, drFlags);
}
public void initializeWithFlags(int drFlags) {
public void initializeWithFlags(int drFlags, int videoFormat) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh) != null)) {
decoderRenderer = new MediaCodecDecoderRenderer(videoFormat);
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
@ -82,12 +84,12 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
}
@Override
public String getDecoderName() {
public boolean isHevcSupported() {
if (decoderRenderer != null) {
return decoderRenderer.getDecoderName();
return decoderRenderer.isHevcSupported();
}
else {
return null;
return false;
}
}
}

View File

@ -3,5 +3,5 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName();
public abstract boolean isHevcSupported();
}

View File

@ -13,6 +13,7 @@ import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.preferences.PreferenceConfiguration;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@ -27,13 +28,17 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
private String avcDecoderName;
private String hevcDecoderName;
private MediaCodec videoDecoder;
private Thread rendererThread;
private final boolean needsSpsBitstreamFixup, isExynos4;
private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private final boolean adaptivePlayback, directSubmit;
private final boolean constrainedHighProfile;
private boolean adaptivePlayback, directSubmit;
private boolean constrainedHighProfile;
private int initialWidth, initialHeight;
private VideoFormat videoFormat;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@ -43,71 +48,141 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private long decoderTimeMs;
private int totalFrames;
private String decoderName;
private int numSpsIn;
private int numPpsIn;
private int numVpsIn;
private int numIframeIn;
public MediaCodecDecoderRenderer() {
private MediaCodecInfo findAvcDecoder() {
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder("video/avc");
}
return decoder;
}
private MediaCodecInfo findHevcDecoder(int videoFormat) {
// Don't return anything if H.265 is forced off
if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
return null;
}
// We don't try the first HEVC decoder. We'd rather fall back to hardware accelerated AVC instead
//
// We need HEVC Main profile, so we could pass that constant to findProbableSafeDecoder, however
// some decoders (at least Qualcomm's Snapdragon 805) don't properly report support
// for even required levels of HEVC.
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (decoderInfo != null) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) {
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) {
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
}
else {
return null;
}
}
}
return decoderInfo;
}
public MediaCodecDecoderRenderer(int videoFormat) {
//dumpDecoders();
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder();
MediaCodecInfo avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
avcDecoderName = avcDecoder.getName();
LimeLog.info("Selected AVC decoder: "+avcDecoderName);
}
if (decoder == null) {
// This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit =
constrainedHighProfile = false;
return;
else {
LimeLog.warning("No AVC decoder found");
}
decoderName = decoder.getName();
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
MediaCodecInfo hevcDecoder = findHevcDecoder(videoFormat);
if (hevcDecoder != null) {
hevcDecoderName = hevcDecoder.getName();
LimeLog.info("Selected HEVC decoder: "+hevcDecoderName);
}
if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
}
if (constrainedHighProfile) {
LimeLog.info("Decoder "+decoderName+" needs constrained high profile");
}
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
else {
LimeLog.info("No HEVC decoder found");
}
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
public boolean isHevcSupported() {
return hevcDecoderName != null;
}
@Override
public boolean setup(VideoDecoderRenderer.VideoFormat format, int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
this.initialHeight = height;
this.videoFormat = format;
if (decoderName == null) {
LimeLog.severe("No available hardware decoder!");
String mimeType;
String selectedDecoderName;
if (videoFormat == VideoFormat.H264) {
mimeType = "video/avc";
selectedDecoderName = avcDecoderName;
if (avcDecoderName == null) {
LimeLog.severe("No available AVC decoder!");
return false;
}
// These fixups only apply to H264 decoders
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(selectedDecoderName);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(selectedDecoderName);
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(selectedDecoderName);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+selectedDecoderName+" needs SPS bitstream restrictions fixup");
}
if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+selectedDecoderName+" needs baseline SPS hack");
}
if (constrainedHighProfile) {
LimeLog.info("Decoder "+selectedDecoderName+" needs constrained high profile");
}
if (isExynos4) {
LimeLog.info("Decoder "+selectedDecoderName+" is on Exynos 4");
}
}
else if (videoFormat == VideoFormat.H265) {
mimeType = "video/hevc";
selectedDecoderName = hevcDecoderName;
if (hevcDecoderName == null) {
LimeLog.severe("No available HEVC decoder!");
return false;
}
}
else {
// Unknown format
return false;
}
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(selectedDecoderName);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(selectedDecoderName);
if (directSubmit) {
LimeLog.info("Decoder "+selectedDecoderName+" will use direct submit");
}
// Codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
videoDecoder = MediaCodec.createByCodecName(decoderName);
videoDecoder = MediaCodec.createByCodecName(selectedDecoderName);
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
MediaFormat videoFormat = MediaFormat.createVideoFormat(mimeType, width, height);
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat
@ -119,7 +194,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
LimeLog.info("Using hardware decoding");
LimeLog.info("Using codec "+selectedDecoderName+" for hardware decoding "+mimeType);
return true;
}
@ -483,6 +558,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferHead();
// H264 SPS
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
@ -585,6 +662,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
depacketizer.freeDecodeUnit(decodeUnit);
return;
// H264 PPS
} else if (header.data[header.offset+4] == 0x68) {
numPpsIn++;
@ -596,6 +675,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
needsSpsReplay = true;
}
}
else if (header.data[header.offset+4] == 0x40) {
numVpsIn++;
}
else if (header.data[header.offset+4] == 0x42) {
numSpsIn++;
}
else if (header.data[header.offset+4] == 0x44) {
numPpsIn++;
}
}
// Copy data from our buffer list into the input buffer
@ -674,11 +762,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return (int)(totalTimeMs / totalFrames);
}
@Override
public String getDecoderName() {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = MediaCodecHelper.getMonotonicMillis();
long delta = currentTime-du.getReceiveTimestamp();
@ -731,9 +814,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
public String toString() {
String str = "";
str += "Decoder: "+renderer.decoderName+"\n";
str += "Format: "+renderer.videoFormat+"\n";
str += "AVC Decoder: "+renderer.avcDecoderName+"\n";
str += "HEVC Decoder: "+renderer.hevcDecoderName+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";

View File

@ -28,6 +28,7 @@ public class MediaCodecHelper {
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
private static final List<String> whitelistedHevcDecoders;
static {
directSubmitPrefixes = new LinkedList<String>();
@ -72,6 +73,12 @@ public class MediaCodecHelper {
constrainedHighProfilePrefixes = new LinkedList<String>();
constrainedHighProfilePrefixes.add("omx.intel");
whitelistedHevcDecoders = new LinkedList<>();
whitelistedHevcDecoders.add("omx.exynos");
whitelistedHevcDecoders.add("omx.qcom");
whitelistedHevcDecoders.add("omx.nvidia");
whitelistedHevcDecoders.add("omx.mtk");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
@ -92,7 +99,7 @@ public class MediaCodecHelper {
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
/*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same
@ -120,21 +127,25 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderNeedsConstrainedHighProfile(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
}
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderCanDirectSubmit(String decoderName) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
@ -199,7 +210,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findFirstDecoder() {
public static MediaCodecInfo findFirstDecoder(String mimeType) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@ -212,9 +223,9 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports H.264
// Find a decoder that supports the specified video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@ -224,7 +235,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findProbableSafeDecoder() {
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
@ -234,12 +245,12 @@ public class MediaCodecHelper {
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder();
return findKnownSafeDecoder(mimeType, requiredProfile);
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder();
return findFirstDecoder(mimeType);
}
}
@ -247,7 +258,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@ -260,21 +271,26 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports H.264 high profile
// Find a decoder that supports the requested video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
if (requiredProfile != -1) {
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == requiredProfile) {
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
return codecInfo;
}
}
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
}
else {
return codecInfo;
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}

View File

@ -21,6 +21,7 @@ public class PreferenceConfiguration {
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@ -42,14 +43,19 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public int width, height, fps;
public int bitrate;
public int decoder;
public int decoder, videoFormat;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
@ -124,6 +130,25 @@ public class PreferenceConfiguration {
}
}
private static int getVideoFormatValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
if (str.equals("auto")) {
return AUTOSELECT_H265;
}
else if (str.equals("forceh265")) {
return FORCE_H265_ON;
}
else if (str.equals("neverh265")) {
return FORCE_H265_OFF;
}
else {
// Should never get here
return AUTOSELECT_H265;
}
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
@ -168,6 +193,7 @@ public class PreferenceConfiguration {
}
config.decoder = getDecoderValue(context);
config.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);

View File

@ -38,4 +38,15 @@
<item>software</item>
<item>hardware</item>
</string-array>
<string-array name="video_format_names">
<item>Use H.265 only if safe</item>
<item>Always use H.265 if available</item>
<item>Never use H.265</item>
</string-array>
<string-array name="video_format_values" translatable="false">
<item>auto</item>
<item>forceh265</item>
<item>neverh265</item>
</string-array>
</resources>

View File

@ -123,4 +123,7 @@
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string>
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
<string name="title_video_format">Change H.265 settings</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device.</string>
</resources>

View File

@ -89,6 +89,13 @@
android:entryValues="@array/decoder_values"
android:summary="@string/summary_decoder_list"
android:defaultValue="auto" />
<ListPreference
android:key="video_format"
android:title="@string/title_video_format"
android:entries="@array/video_format_names"
android:entryValues="@array/video_format_values"
android:summary="@string/summary_video_format"
android:defaultValue="auto" />
</PreferenceCategory>
</PreferenceScreen>