Add a checkbox to favor image quality over performance

This commit is contained in:
Cameron Gutman 2013-11-26 20:03:55 -05:00
parent 8e22a125e2
commit b603bf4887
12 changed files with 91 additions and 40 deletions

View File

@ -35,10 +35,11 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
public static final int ic_launcher=0x7f020000; public static final int ic_launcher=0x7f020000;
} }
public static final class id { public static final class id {
public static final int hostTextView=0x7f080002; public static final int hostTextView=0x7f080000;
public static final int pairButton=0x7f080000; public static final int imageQualityCheckbox=0x7f080002;
public static final int pairButton=0x7f080003;
public static final int statusButton=0x7f080001; public static final int statusButton=0x7f080001;
public static final int surfaceView=0x7f080003; public static final int surfaceView=0x7f080004;
} }
public static final class layout { public static final class layout {
public static final int activity_connection=0x7f030000; public static final int activity_connection=0x7f030000;

View File

@ -8,14 +8,6 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".Connection" > tools:context=".Connection" >
<Button
android:id="@+id/pairButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/statusButton"
android:layout_centerHorizontal="true"
android:text="Pair with PC"/>
<EditText <EditText
android:id="@+id/hostTextView" android:id="@+id/hostTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -40,5 +32,20 @@
</Button> </Button>
<CheckBox
android:id="@+id/imageQualityCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/hostTextView"
android:layout_below="@+id/pairButton"
android:text="Prefer image quality over performance" />
<Button
android:id="@+id/pairButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/statusButton"
android:layout_centerHorizontal="true"
android:text="Pair with PC" />
</RelativeLayout> </RelativeLayout>

View File

@ -5,7 +5,7 @@
Base application theme for API 11+. This theme completely replaces Base application theme for API 11+. This theme completely replaces
AppBaseTheme from res/values/styles.xml on API 11+ devices. AppBaseTheme from res/values/styles.xml on API 11+ devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Holo.Light"> <style name="AppBaseTheme" parent="android:Theme.Holo">
<!-- API 11 theme customizations can go here. --> <!-- API 11 theme customizations can go here. -->
</style> </style>

View File

@ -5,7 +5,7 @@
AppBaseTheme from BOTH res/values/styles.xml and AppBaseTheme from BOTH res/values/styles.xml and
res/values-v11/styles.xml on API 14+ devices. res/values-v11/styles.xml on API 14+ devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar"> <style name="AppBaseTheme" parent="android:Theme.Holo">
<!-- API 14 theme customizations can go here. --> <!-- API 14 theme customizations can go here. -->
</style> </style>

View File

@ -5,7 +5,7 @@
Base application theme, dependent on API level. This theme is replaced Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices. by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Light"> <style name="AppBaseTheme" parent="android:Theme">
<!-- <!--
Theme customizations available in newer API levels can go in Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to res/values-vXX/styles.xml, while customizations related to

View File

@ -17,9 +17,13 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -27,6 +31,7 @@ public class Connection extends Activity {
private Button statusButton, pairButton; private Button statusButton, pairButton;
private TextView hostText; private TextView hostText;
private SharedPreferences prefs; private SharedPreferences prefs;
private CheckBox qualityCheckbox;
private static final String DEFAULT_HOST = ""; private static final String DEFAULT_HOST = "";
public static final String HOST_KEY = "hostText"; public static final String HOST_KEY = "hostText";
@ -57,9 +62,20 @@ public class Connection extends Activity {
this.statusButton = (Button) findViewById(R.id.statusButton); this.statusButton = (Button) findViewById(R.id.statusButton);
this.pairButton = (Button) findViewById(R.id.pairButton); this.pairButton = (Button) findViewById(R.id.pairButton);
this.hostText = (TextView) findViewById(R.id.hostTextView); this.hostText = (TextView) findViewById(R.id.hostTextView);
this.qualityCheckbox = (CheckBox) findViewById(R.id.imageQualityCheckbox);
prefs = getPreferences(0); prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST)); this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
this.qualityCheckbox.setChecked(prefs.getBoolean(Game.QUALITY_PREF_STRING, false));
this.qualityCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(Game.QUALITY_PREF_STRING, isChecked);
editor.commit();
}
});
this.statusButton.setOnClickListener(new OnClickListener() { this.statusButton.setOnClickListener(new OnClickListener() {
@Override @Override

View File

@ -2,11 +2,14 @@ package com.limelight;
import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.av.video.DecoderRenderer;
import com.limelight.nvstream.input.NvControllerPacket; import com.limelight.nvstream.input.NvControllerPacket;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.os.Bundle; import android.os.Bundle;
import android.view.InputDevice; import android.view.InputDevice;
@ -39,6 +42,9 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
private SpinnerDialog spinner; private SpinnerDialog spinner;
private boolean displayedFailureDialog = false; private boolean displayedFailureDialog = false;
public static final String PREFS_FILE_NAME = "gameprefs";
public static final String QUALITY_PREF_STRING = "Quality";
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -67,8 +73,15 @@ public class Game extends Activity implements OnGenericMotionListener, OnTouchLi
// Start the spinner // Start the spinner
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true); spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
// Read the stream preferences
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
int drFlags = 0;
if (prefs.getBoolean(QUALITY_PREF_STRING, false)) {
drFlags |= DecoderRenderer.FLAG_PREFER_QUALITY;
}
// Start the connection // Start the connection
conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this, sv.getHolder().getSurface()); conn = new NvConnection(Game.this.getIntent().getStringExtra("host"), Game.this, sv.getHolder(), drFlags);
conn.start(); conn.start();
} }

View File

@ -14,7 +14,7 @@ import org.xmlpull.v1.XmlPullParserException;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.view.Surface; import android.view.SurfaceHolder;
import android.widget.Toast; import android.widget.Toast;
import com.limelight.Game; import com.limelight.Game;
@ -24,22 +24,24 @@ public class NvConnection {
private String host; private String host;
private Game activity; private Game activity;
private NvConnectionListener listener; private NvConnectionListener listener;
private int drFlags;
private InetAddress hostAddr; private InetAddress hostAddr;
private NvControl controlStream; private NvControl controlStream;
private NvController inputStream; private NvController inputStream;
private Surface video; private SurfaceHolder video;
private NvVideoStream videoStream; private NvVideoStream videoStream;
private NvAudioStream audioStream; private NvAudioStream audioStream;
private ThreadPoolExecutor threadPool; private ThreadPoolExecutor threadPool;
public NvConnection(String host, Game activity, Surface video) public NvConnection(String host, Game activity, SurfaceHolder video, int drFlags)
{ {
this.host = host; this.host = host;
this.listener = activity; this.listener = activity;
this.activity = activity; this.activity = activity;
this.video = video; this.video = video;
this.drFlags = drFlags;
this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>()); this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
} }
@ -148,7 +150,7 @@ public class NvConnection {
private boolean startVideoStream() throws IOException private boolean startVideoStream() throws IOException
{ {
videoStream = new NvVideoStream(hostAddr, listener, controlStream); videoStream = new NvVideoStream(hostAddr, listener, controlStream);
videoStream.startVideoStream(video); videoStream.startVideoStream(video, drFlags);
return true; return true;
} }

View File

@ -22,7 +22,7 @@ import com.limelight.nvstream.av.video.DecoderRenderer;
import com.limelight.nvstream.av.video.MediaCodecDecoderRenderer; import com.limelight.nvstream.av.video.MediaCodecDecoderRenderer;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.SurfaceHolder;
public class NvVideoStream { public class NvVideoStream {
public static final int RTP_PORT = 47998; public static final int RTP_PORT = 47998;
@ -129,7 +129,7 @@ public class NvVideoStream {
rtp = new DatagramSocket(RTP_PORT); rtp = new DatagramSocket(RTP_PORT);
} }
public void setupDecoderRenderer(Surface renderTarget) { public void setupDecoderRenderer(SurfaceHolder renderTarget, int drFlags) {
if (Build.HARDWARE.equals("goldfish")) { if (Build.HARDWARE.equals("goldfish")) {
// Emulator - don't render video (it's slow!) // Emulator - don't render video (it's slow!)
decrend = null; decrend = null;
@ -144,14 +144,14 @@ public class NvVideoStream {
} }
if (decrend != null) { if (decrend != null) {
decrend.setup(1280, 720, renderTarget); decrend.setup(1280, 720, renderTarget, drFlags);
} }
} }
public void startVideoStream(final Surface surface) throws IOException public void startVideoStream(final SurfaceHolder surface, int drFlags) throws IOException
{ {
// Setup the decoder and renderer // Setup the decoder and renderer
setupDecoderRenderer(surface); setupDecoderRenderer(surface, drFlags);
// Open RTP sockets and start session // Open RTP sockets and start session
setupRtpSession(); setupRtpSession();

View File

@ -5,7 +5,9 @@ import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder;
import com.limelight.nvstream.av.AvByteBufferDescriptor; import com.limelight.nvstream.av.AvByteBufferDescriptor;
import com.limelight.nvstream.av.AvDecodeUnit; import com.limelight.nvstream.av.AvDecodeUnit;
@ -74,24 +76,24 @@ public class CpuDecoderRenderer implements DecoderRenderer {
} }
@Override @Override
public void setup(int width, int height, Surface renderTarget) { public void setup(int width, int height, SurfaceHolder renderTarget, int drFlags) {
this.renderTarget = renderTarget; this.renderTarget = renderTarget.getSurface();
this.targetFps = 30; this.targetFps = 30;
int perfLevel = findOptimalPerformanceLevel(); int perfLevel = findOptimalPerformanceLevel();
int threadCount; int threadCount;
int flags = 0; int avcFlags = 0;
switch (perfLevel) { switch (perfLevel) {
case HIGH_PERF: case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive // Single threaded low latency decode is ideal but hard to acheive
flags = AvcDecoder.LOW_LATENCY_DECODE; avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1; threadCount = 1;
break; break;
case LOW_PERF: case LOW_PERF:
// Disable the loop filter for performance reasons // Disable the loop filter for performance reasons
flags = AvcDecoder.DISABLE_LOOP_FILTER | avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING | AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE; AvcDecoder.FAST_DECODE;
@ -101,7 +103,7 @@ public class CpuDecoderRenderer implements DecoderRenderer {
default: default:
case MED_PERF: case MED_PERF:
flags = AvcDecoder.BILINEAR_FILTERING | avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE; AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency // Only use 2 threads to minimize frame processing latency
@ -109,7 +111,15 @@ public class CpuDecoderRenderer implements DecoderRenderer {
break; break;
} }
int err = AvcDecoder.init(width, height, flags, threadCount); // If the user wants quality, we'll remove the low IQ flags
if ((drFlags & DecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
System.out.println("Using high quality decoding");
}
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) { if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }

View File

@ -2,10 +2,12 @@ package com.limelight.nvstream.av.video;
import com.limelight.nvstream.av.AvDecodeUnit; import com.limelight.nvstream.av.AvDecodeUnit;
import android.view.Surface; import android.view.SurfaceHolder;
public interface DecoderRenderer { public interface DecoderRenderer {
public void setup(int width, int height, Surface renderTarget); public static int FLAG_PREFER_QUALITY = 0x1;
public void setup(int width, int height, SurfaceHolder renderTarget, int drFlags);
public void start(); public void start();

View File

@ -14,7 +14,7 @@ import android.media.MediaCodecList;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo; import android.media.MediaCodec.BufferInfo;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.SurfaceHolder;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class MediaCodecDecoderRenderer implements DecoderRenderer { public class MediaCodecDecoderRenderer implements DecoderRenderer {
@ -73,11 +73,11 @@ public class MediaCodecDecoderRenderer implements DecoderRenderer {
} }
@Override @Override
public void setup(int width, int height, Surface renderTarget) { public void setup(int width, int height, SurfaceHolder renderTarget, int drFlags) {
videoDecoder = MediaCodec.createByCodecName(findSafeDecoder().getName()); videoDecoder = MediaCodec.createByCodecName(findSafeDecoder().getName());
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height); MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
videoDecoder.configure(videoFormat, renderTarget, null, 0); videoDecoder.configure(videoFormat, renderTarget.getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);