Skip to content

Commit c47725d

Browse files
author
Chris Bellew
committed
Added bitrate spinner to video player media controller.
1 parent 201a785 commit c47725d

File tree

6 files changed

+226
-9
lines changed

6 files changed

+226
-9
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.atomjack.vcfp;
2+
3+
import android.content.Context;
4+
import android.support.v7.widget.AppCompatSpinner;
5+
import android.util.AttributeSet;
6+
7+
public class CustomSpinner extends AppCompatSpinner {
8+
private OnSpinnerEventsListener mListener;
9+
private boolean mOpenInitiated = false;
10+
11+
public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
12+
super(context, attrs, defStyleAttr, mode);
13+
}
14+
15+
public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
16+
super(context, attrs, defStyleAttr);
17+
}
18+
19+
public CustomSpinner(Context context, AttributeSet attrs) {
20+
super(context, attrs);
21+
}
22+
23+
public CustomSpinner(Context context, int mode) {
24+
super(context, mode);
25+
}
26+
27+
public CustomSpinner(Context context) {
28+
super(context);
29+
}
30+
31+
public interface OnSpinnerEventsListener {
32+
void onSpinnerOpened();
33+
void onSpinnerClosed();
34+
}
35+
36+
@Override
37+
public boolean performClick() {
38+
// register that the Spinner was opened so we have a status
39+
// indicator for the activity(which may lose focus for some other
40+
// reasons)
41+
mOpenInitiated = true;
42+
if (mListener != null) {
43+
mListener.onSpinnerOpened();
44+
}
45+
return super.performClick();
46+
}
47+
48+
public void setSpinnerEventsListener(OnSpinnerEventsListener onSpinnerEventsListener) {
49+
mListener = onSpinnerEventsListener;
50+
}
51+
52+
/**
53+
* Propagate the closed Spinner event to the listener from outside.
54+
*/
55+
public void performClosedEvent() {
56+
mOpenInitiated = false;
57+
if (mListener != null) {
58+
mListener.onSpinnerClosed();
59+
}
60+
}
61+
62+
/**
63+
* A boolean flag indicating that the Spinner triggered an open event.
64+
*
65+
* @return true for opened Spinner
66+
*/
67+
public boolean hasBeenOpened() {
68+
return mOpenInitiated;
69+
}
70+
71+
@Override
72+
public void onWindowFocusChanged(boolean hasWindowFocus) {
73+
super.onWindowFocusChanged(hasWindowFocus);
74+
if (hasBeenOpened() && hasWindowFocus) {
75+
performClosedEvent();
76+
}
77+
}
78+
}

mobile/src/main/java/com/atomjack/vcfp/VideoControllerView.java

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818

1919
import android.content.Context;
2020
import android.graphics.Bitmap;
21+
import android.graphics.PorterDuff;
2122
import android.os.Handler;
2223
import android.os.Message;
24+
import android.support.v4.content.ContextCompat;
2325
import android.util.AttributeSet;
2426
import android.util.Log;
2527
import android.view.Gravity;
@@ -30,6 +32,8 @@
3032
import android.view.ViewGroup;
3133
import android.view.accessibility.AccessibilityEvent;
3234
import android.view.accessibility.AccessibilityNodeInfo;
35+
import android.widget.AdapterView;
36+
import android.widget.ArrayAdapter;
3337
import android.widget.FrameLayout;
3438
import android.widget.ImageButton;
3539
import android.widget.ImageView;
@@ -38,7 +42,10 @@
3842
import android.widget.SeekBar.OnSeekBarChangeListener;
3943
import android.widget.TextView;
4044

45+
import com.atomjack.shared.Logger;
46+
4147
import java.lang.ref.WeakReference;
48+
import java.util.ArrayList;
4249
import java.util.Formatter;
4350
import java.util.Locale;
4451

@@ -104,6 +111,11 @@ public class VideoControllerView extends FrameLayout {
104111
private ImageView mPoster;
105112
private Bitmap mPosterBitmap;
106113
private ViewGroup mMediaControllerPosterContainer;
114+
private CustomSpinner mBitrateSpinner;
115+
private boolean mBitrateSpinnerIsOpen = false;
116+
private String mCurrentVideoQuality;
117+
118+
private BitrateChangeListener mBitrateChangeListener;
107119

108120
public VideoControllerView(Context context, AttributeSet attrs) {
109121
super(context, attrs);
@@ -125,7 +137,6 @@ public VideoControllerView(Context context, boolean useFastForward) {
125137

126138
public VideoControllerView(Context context) {
127139
this(context, true);
128-
129140
Log.i(TAG, TAG);
130141
}
131142

@@ -242,6 +253,69 @@ public boolean onTouch(View v, MotionEvent event) {
242253
if(mPosterBitmap != null)
243254
mPoster.setImageBitmap(mPosterBitmap);
244255

256+
mBitrateSpinner = (CustomSpinner) v.findViewById(R.id.bitrateSpinner);
257+
if(mBitrateSpinner != null) {
258+
259+
mBitrateSpinner.getBackground().setColorFilter(ContextCompat.getColor(mContext, R.color.white), PorterDuff.Mode.SRC_ATOP);
260+
261+
final ArrayList<String> list = new ArrayList<>(VoiceControlForPlexApplication.localVideoQualityOptions.keySet());
262+
ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, R.layout.bitrate_dropdown_item, list) {
263+
@Override
264+
public View getView(int position, View convertView, ViewGroup parent) {
265+
TextView textView = (TextView)super.getView(position, convertView, parent);
266+
textView.setTextColor(ContextCompat.getColor(mContext, R.color.white));
267+
return textView;
268+
}
269+
270+
@Override
271+
public View getDropDownView(int position, View convertView, ViewGroup parent) {
272+
TextView textView = (TextView)super.getDropDownView(position, convertView, parent);
273+
textView.setTextColor(ContextCompat.getColor(mContext, textView.getText().equals(mCurrentVideoQuality) ? R.color.black : R.color.white));
274+
textView.setBackgroundColor(ContextCompat.getColor(mContext, textView.getText().equals(mCurrentVideoQuality) ? R.color.white : R.color.mediaControllerBackground));
275+
return textView;
276+
}
277+
};
278+
mBitrateSpinner.setAdapter(adapter);
279+
280+
mBitrateSpinner.setSelection(list.indexOf(mCurrentVideoQuality), false);
281+
282+
mBitrateSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
283+
@Override
284+
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
285+
String item = list.get(position);
286+
// Only notify the activity if the bitrate clicked on is not the current one
287+
if(mBitrateChangeListener != null && !item.equals(mCurrentVideoQuality)) {
288+
hide();
289+
mBitrateChangeListener.onBitrateChange(item);
290+
}
291+
}
292+
293+
@Override
294+
public void onNothingSelected(AdapterView<?> parent) {
295+
296+
}
297+
});
298+
299+
300+
mBitrateSpinner.setSpinnerEventsListener(new CustomSpinner.OnSpinnerEventsListener() {
301+
@Override
302+
public void onSpinnerOpened() {
303+
mBitrateSpinnerIsOpen = true;
304+
}
305+
306+
@Override
307+
public void onSpinnerClosed() {
308+
mBitrateSpinnerIsOpen = false;
309+
// hide the controller after 3 seconds
310+
Message msg = mHandler.obtainMessage(FADE_OUT);
311+
mHandler.removeMessages(FADE_OUT);
312+
mHandler.sendMessageDelayed(msg, sDefaultTimeout);
313+
}
314+
});
315+
316+
317+
}
318+
245319
mProgress = (SeekBar) v.findViewById(R.id.mediacontroller_progress);
246320
if (mProgress != null) {
247321
if (mProgress instanceof SeekBar) {
@@ -326,7 +400,7 @@ public void show(int timeout) {
326400
mHandler.sendEmptyMessage(SHOW_PROGRESS);
327401

328402
Message msg = mHandler.obtainMessage(FADE_OUT);
329-
if (timeout != 0) {
403+
if (timeout != 0 ) {
330404
mHandler.removeMessages(FADE_OUT);
331405
mHandler.sendMessageDelayed(msg, timeout);
332406
}
@@ -738,7 +812,8 @@ public void handleMessage(Message msg) {
738812
int pos;
739813
switch (msg.what) {
740814
case FADE_OUT:
741-
view.hide();
815+
if(!view.mBitrateSpinnerIsOpen)
816+
view.hide();
742817
break;
743818
case SHOW_PROGRESS:
744819
pos = view.setProgress();
@@ -756,4 +831,16 @@ public void setPoster(Bitmap bitmap) {
756831
if(mPoster != null)
757832
mPoster.setImageBitmap(bitmap);
758833
}
834+
835+
public void setCurrentVideoQuality(String videoQuality) {
836+
mCurrentVideoQuality = videoQuality;
837+
}
838+
839+
public void setBitrateChangeListener(BitrateChangeListener listener) {
840+
mBitrateChangeListener = listener;
841+
}
842+
843+
public interface BitrateChangeListener {
844+
void onBitrateChange(String bitrate);
845+
}
759846
}

mobile/src/main/java/com/atomjack/vcfp/activities/VideoPlayerActivity.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ public class VideoPlayerActivity extends AppCompatActivity
5454
MediaPlayer.OnPreparedListener,
5555
MediaPlayer.OnErrorListener,
5656
SurfaceHolder.Callback,
57-
VideoControllerView.MediaPlayerControl {
57+
VideoControllerView.MediaPlayerControl,
58+
VideoControllerView.BitrateChangeListener {
5859

5960
private NewLogger logger;
6061

@@ -84,6 +85,8 @@ public class VideoPlayerActivity extends AppCompatActivity
8485
// data source, then start playback again
8586
private Runnable finishOnPlayerStop = null;
8687

88+
private String currentBitrate = null;
89+
8790
SurfaceView videoSurface;
8891
VideoControllerView controller;
8992

@@ -251,8 +254,10 @@ public void run() {
251254

252255
player.setOnErrorListener(VideoPlayerActivity.this);
253256
player.setOnCompletionListener(VideoPlayerActivity.this);
254-
if(controller == null)
257+
if(controller == null) {
255258
controller = new VideoControllerView(VideoPlayerActivity.this);
259+
controller.setBitrateChangeListener(VideoPlayerActivity.this);
260+
}
256261
logger.d("Have %d videos", playlist.size());
257262
if(playlist.size() > 1) // Only show the prev/next buttons when there is more than one video to play
258263
controller.setPrevNextListeners(currentVideoIndex > 0 ? onPrevious : null, currentVideoIndex+1 < playlist.size() ? onNext : null);
@@ -367,8 +372,13 @@ private String getTranscodeUrl(PlexMedia media, Connection connection, String tr
367372
qs.add("directPlay", "0");
368373
qs.add("directStream", "1");
369374
qs.add("videoQuality", "60");
370-
qs.add("maxVideoBitrate", VoiceControlForPlexApplication.localVideoQualityOptions.get(VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.LOCAL_VIDEO_QUALITY_LOCAL : Preferences.LOCAL_VIDEO_QUALITY_REMOTE))[0]);
371-
qs.add("videoResolution", VoiceControlForPlexApplication.localVideoQualityOptions.get(VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.LOCAL_VIDEO_QUALITY_LOCAL : Preferences.LOCAL_VIDEO_QUALITY_REMOTE))[1]);
375+
376+
if(currentBitrate == null) {
377+
currentBitrate = VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.LOCAL_VIDEO_QUALITY_LOCAL : Preferences.LOCAL_VIDEO_QUALITY_REMOTE);
378+
}
379+
380+
qs.add("maxVideoBitrate", VoiceControlForPlexApplication.localVideoQualityOptions.get(currentBitrate)[0]);
381+
qs.add("videoResolution", VoiceControlForPlexApplication.localVideoQualityOptions.get(currentBitrate)[1]);
372382
qs.add("audioBoost", "100");
373383
qs.add("session", session);
374384
qs.add(PlexHeaders.XPlexClientIdentifier, VoiceControlForPlexApplication.getInstance().prefs.getUUID());
@@ -418,6 +428,8 @@ public void onPrepared(MediaPlayer mp) {
418428
logger.d("onPrepared");
419429
currentState = PlayerState.BUFFERING;
420430

431+
controller.setCurrentVideoQuality(currentBitrate);
432+
421433
controller.setMediaPlayer(this);
422434
controller.setAnchorView((FrameLayout) findViewById(R.id.videoSurfaceContainer));
423435

@@ -751,4 +763,16 @@ public void run() {
751763
stop();
752764
}
753765
};
766+
767+
// Implement BitrateChangeListener
768+
769+
@Override
770+
public void onBitrateChange(String bitrate) {
771+
currentBitrate = bitrate;
772+
playNewVideo();
773+
}
774+
775+
// End Implement BitrateChangeListener
776+
777+
754778
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:id="@android:id/text1"
4+
style="?android:attr/spinnerItemStyle"
5+
android:layout_width="match_parent"
6+
android:layout_height="40dp"
7+
android:gravity="center_vertical"
8+
android:ellipsize="marquee"
9+
android:singleLine="true"
10+
android:textSize="14sp"
11+
android:textColor="@color/white"
12+
android:textAlignment="inherit"/>

mobile/src/main/res/layout/media_controller.xml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,27 @@
3737
android:layout_alignParentRight="true"
3838
android:layout_alignParentEnd="true"/>
3939

40+
<FrameLayout
41+
android:layout_width="wrap_content"
42+
android:layout_height="wrap_content"
43+
android:background="@color/mediaControllerBackground"
44+
android:layout_alignParentBottom="true"
45+
android:layout_alignParentLeft="true"
46+
android:layout_alignParentStart="true">
47+
48+
<com.atomjack.vcfp.CustomSpinner
49+
android:layout_width="wrap_content"
50+
android:layout_height="wrap_content"
51+
android:id="@+id/bitrateSpinner"/>
52+
</FrameLayout>
53+
4054
</RelativeLayout>
4155

4256
<LinearLayout
4357
android:orientation="vertical"
4458
android:layout_width="match_parent"
4559
android:layout_height="match_parent"
46-
android:background="#CC000000"
60+
android:background="@color/mediaControllerBackground"
4761
android:paddingTop="20dp"
4862
android:paddingBottom="20dp">
4963

@@ -60,7 +74,8 @@
6074
android:layout_height="wrap_content"
6175
android:id="@+id/mic"
6276
android:src="@drawable/button_microphone"
63-
android:background="@android:color/transparent"/>
77+
android:background="@android:color/transparent"
78+
android:layout_gravity="center_vertical"/>
6479

6580
<SeekBar
6681
android:id="@+id/mediacontroller_progress"

mobile/src/main/res/values/colors.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@
2323
<color name="primary_600">#1E88E5</color>
2424
<color name="primary">#ffffff</color>
2525
<color name="primaryDark">#0000ff</color>
26+
<color name="mediaControllerBackground">#CC000000</color>
2627
</resources>

0 commit comments

Comments
 (0)