Android TV 开发之 TV视频播放器(下)

简介: Android TV 开发之 TV视频播放器(下)

了解这个之后,我们还得知道遥控器的按键监听,毕竟是用遥控器来操作的啊,按键监听代码如下:

private String TAG = "key";
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:     //确定键enter
            case KeyEvent.KEYCODE_DPAD_CENTER:
                Log.d(TAG, "enter--->");
                break;
            case KeyEvent.KEYCODE_BACK:    //返回键
                Log.d(TAG,"back--->");
                return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
            case KeyEvent.KEYCODE_SETTINGS: //设置键
                Log.d(TAG, "setting--->");
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:   //向下键
                /*    实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发
                 *    exp:KeyEvent.ACTION_UP
                 */
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    Log.d(TAG, "down--->");
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:   //向上键
                Log.d(TAG, "up--->");
                break;
            case KeyEvent.KEYCODE_0:   //数字键0
                Log.d(TAG, "0--->");
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT: //向左键
                Log.d(TAG, "left--->");
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:  //向右键
                Log.d(TAG, "right--->");
                break;
            case KeyEvent.KEYCODE_INFO:    //info键
                Log.d(TAG, "info--->");
                break;
            case KeyEvent.KEYCODE_PAGE_DOWN:     //向上翻页键
            case KeyEvent.KEYCODE_MEDIA_NEXT:
                Log.d(TAG, "page down--->");
                break;
            case KeyEvent.KEYCODE_PAGE_UP:     //向下翻页键
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                Log.d(TAG, "page up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_UP:   //调大声音键
                Log.d(TAG, "voice up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键
                Log.d(TAG, "voice down--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音
                Log.d(TAG, "voice mute--->");
                break;
            default:
                break;
        }
        return super.onKeyDown(keyCode, event);
    }


如果你要监听Home键的话,就需要通过广播来,

在MainActivity中创建一个class


class  HomeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if(action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){
                String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
                if(SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)){
                    Toast.makeText(MainActivity.this,"home键触发",Toast.LENGTH_SHORT).show();
                    Log.d(TAG, "home键触发");
                }
            }
        }
    }


在onCreate()方法中注册广播,只要调用initReceiver()方法即可


  public final String SYSTEM_DIALOG_REASON_KEY = "reason";
    public final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
    private HomeReceiver homeReceiver;
    /**
     * 注册广播
     */
    private void initReceiver() {
        homeReceiver = new HomeReceiver();
        IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        registerReceiver(homeReceiver, filter);
    }


页面销毁时,注销掉广播


@Override
    protected void onDestroy() {
        super.onDestroy();
        if(homeReceiver!=null){
            unregisterReceiver(homeReceiver);
        }
    }


这段代码我也是从网上找的,

然后我们在确定键的下面弹出这个Toast


case KeyEvent.KEYCODE_ENTER:     //确定键enter
            case KeyEvent.KEYCODE_DPAD_CENTER:
                Log.d(TAG, "enter--->");
                Toast.makeText(this,tvTest.getText().toString(),Toast.LENGTH_SHORT).show();
                break;


运行效果如下:


20200309170503394.png


MainActivity.java完整代码如下:


package com.llw.androidtvdemo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_test)
    TextView tvTest;
    @BindView(R.id.btn_test)
    Button btnTest;
    public final String SYSTEM_DIALOG_REASON_KEY = "reason";
    public final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
    private HomeReceiver homeReceiver;
    /**
     * 注册广播
     */
    private void initReceiver() {
        homeReceiver = new HomeReceiver();
        IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        registerReceiver(homeReceiver, filter);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        btnTest.setFocusable(true);
        initReceiver();
    }
    @OnClick(R.id.btn_test)
    public void onViewClicked() {
        //Toast 提示
        Toast.makeText(this,tvTest.getText().toString(),Toast.LENGTH_SHORT).show();
    }
    class  HomeReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if(action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){
                String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
                if(SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)){
                    Toast.makeText(MainActivity.this,"home键触发",Toast.LENGTH_SHORT).show();
                    Log.d(TAG, "home键触发");
                }
            }
        }
    }
    private String TAG = "key";
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:     //确定键enter
            case KeyEvent.KEYCODE_DPAD_CENTER:
                Log.d(TAG, "enter--->");
                Toast.makeText(this,tvTest.getText().toString(),Toast.LENGTH_SHORT).show();
                break;
            case KeyEvent.KEYCODE_BACK:    //返回键
                Log.d(TAG,"back--->");
                return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
            case KeyEvent.KEYCODE_SETTINGS: //设置键
                Log.d(TAG, "setting--->");
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:   //向下键
                /*    实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发
                 *    exp:KeyEvent.ACTION_UP
                 */
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    Log.d(TAG, "down--->");
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:   //向上键
                Log.d(TAG, "up--->");
                break;
            case KeyEvent.KEYCODE_0:   //数字键0
                Log.d(TAG, "0--->");
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT: //向左键
                Log.d(TAG, "left--->");
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:  //向右键
                Log.d(TAG, "right--->");
                break;
            case KeyEvent.KEYCODE_INFO:    //info键
                Log.d(TAG, "info--->");
                break;
            case KeyEvent.KEYCODE_PAGE_DOWN:     //向上翻页键
            case KeyEvent.KEYCODE_MEDIA_NEXT:
                Log.d(TAG, "page down--->");
                break;
            case KeyEvent.KEYCODE_PAGE_UP:     //向下翻页键
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                Log.d(TAG, "page up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_UP:   //调大声音键
                Log.d(TAG, "voice up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键
                Log.d(TAG, "voice down--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音
                Log.d(TAG, "voice mute--->");
                break;
            default:
                break;
        }
        return super.onKeyDown(keyCode, event);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(homeReceiver!=null){
            unregisterReceiver(homeReceiver);
        }
    }
}


然后我们就要想一下编码的过程和逻辑问题了,

1.播放视频的来源 本地 和 网络

2.播放视频的的停止播放、继续播放、重新播放

3.播放视频时的时间和进度计算

4.播放时候按遥控器左右键时,前进 后退

先想清楚这些问题,才能使编码过程中变得有条理


视频来源


本地:


我们可以在valuse文件夹下面创建一个raw文件夹,在里面放一个mp4短视频文件,(PS:至于在真机存储里面放一个视频,你只要播放路径指定这个视频所在地址,然后再加上文件的读写权限,因为我不是这么实现的,所以就不过多赘述了)


网络:


就是通过一个视频地址来播放视频,既然是通过网络来播放的,我们肯定要有联网的权限啊,在AndroidManifest.xml文件中添加联网许可权限

如下所示


<uses-permission android:name="android.permission.INTERNET" />

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!--自定义的VideoView  做了绘制改变,和网络地址许可-->
        <com.llw.androidtvdemo.view.MyVideoView
            android:id="@+id/video_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center" />
    </LinearLayout>
    <!--底部控制栏  开始时间 进度条 结束时间-->
    <RelativeLayout
        android:background="@drawable/shape_gradual_change"
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_100">
        <LinearLayout
            android:gravity="center_vertical"
            android:layout_margin="@dimen/dp_10"
            android:layout_alignParentBottom="true"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <TextView
                android:id="@+id/tv_play_time"
                android:text="00:00"
                android:textSize="@dimen/sp_24"
                android:textColor="@color/white"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
            <SeekBar
                android:layout_marginLeft="@dimen/dp_20"
                android:layout_marginRight="@dimen/dp_20"
                android:id="@+id/time_seekBar"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:max="100"
                android:maxHeight="3dp"
                android:minHeight="3dp"
                android:progress="0"
                android:progressDrawable="@drawable/seekbar_style"
                android:thumb="@drawable/thumb" />
            <TextView
                android:id="@+id/tv_total_time"
                android:text="00:00"
                android:textSize="@dimen/sp_24"
                android:textColor="@color/white"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
        </LinearLayout>
    </RelativeLayout>
    <!--视频结束时 显示黑色背景-->
    <RelativeLayout
        android:visibility="gone"
        android:id="@+id/lay_finish_bg"
        android:background="#000"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <!--视频播放中 控制暂停和播放的按钮-->
    <ImageButton
        android:visibility="gone"
        android:focusable="true"
        android:layout_centerInParent="true"
        android:id="@+id/btn_play_or_pause"
        android:background="@mipmap/icon_pause"
        android:layout_width="@dimen/dp_100"
        android:layout_height="@dimen/dp_100"/>
    <!--视频结束时 显示重播图标-->
    <ImageButton
        android:visibility="gone"
        android:layout_centerInParent="true"
        android:id="@+id/btn_restart_play"
        android:background="@mipmap/icon_restart_play"
        android:layout_width="@dimen/dp_100"
        android:layout_height="@dimen/dp_100"/>
</RelativeLayout>


注释已经加在布局文件里面了,下面就不过多讲述了,布局文件中的自定义VideoView代码如下:

package com.llw.androidtvdemo.view;
import android.content.Context;
import android.net.Uri;
import android.util.AttributeSet;
import android.widget.VideoView;
import com.llw.androidtvdemo.view.util.SSlUtiles;
import javax.net.ssl.HttpsURLConnection;
/**
 * 自定义VideoView
 */
public class MyVideoView extends VideoView {
    public MyVideoView(Context context) {
        super(context);
    }
    public MyVideoView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getDefaultSize(getWidth(), widthMeasureSpec);
        int height = getDefaultSize(getHeight(), heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
    @Override
    public void setVideoURI(Uri uri) {
        super.setVideoURI(uri);
        try {
            HttpsURLConnection.setDefaultSSLSocketFactory(SSlUtiles.createSSLSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new SSlUtiles.TrustAllHostnameVerifier());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


自定义VideoView中SSlUtils网络证书许可类代码如下:

package com.llw.androidtvdemo.view.util;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class SSlUtils {
    /**
     * 默认信任所有的证书
     *
     * xts
     */
    public static SSLSocketFactory createSSLSocketFactory() {
        SSLSocketFactory sSLSocketFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[] { (TrustManager) new TrustAllManager() }, new SecureRandom());
            sSLSocketFactory = sc.getSocketFactory();
        } catch (Exception e) {
        }
        return sSLSocketFactory;
    }
    public static class TrustAllManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }
    public static class TrustAllHostnameVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }
}


这个类主要是针对于 VideoView 无法播放此视频 问题,如果你没有这个问题的话,可以在MyVideoView去掉下面这一段代码:


@Override
    public void setVideoURI(Uri uri) {
        super.setVideoURI(uri);
        try {
            HttpsURLConnection.setDefaultSSLSocketFactory(SSlUtils.createSSLSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new SSlUtils.TrustAllHostnameVerifier());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


然后来看MainActivity中的代码,通过注解的方式我的控件已经不需要声明和findById了。

首先配置一下我们的VideoVIew


/**
     * 初始化VideoView
     */
    private void initVideo() {
        //本地视频
//        videoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/raw/test"));
        //网络视频
        final Uri uri = Uri.parse("http://gslb.miaopai.com/stream/ed5HCfnhovu3tyIQAiv60Q__.mp4");
        videoView.setVideoURI(uri);
        videoView.requestFocus();
        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                int totalTime = videoView.getDuration();//获取视频的总时长
                tvTotalTime.setText(stringForTime(totalTime));//设置视频总时间,stringForTime是写的一个时间装换方法,下面会提到
                // 开始线程,更新进度条的刻度
                handler.postDelayed(runnable, 0);
                timeSeekBar.setMax(videoView.getDuration());
                //视频加载完成,准备好播放视频的回调
                videoView.start();
            }
        });
    }


上面的初始化中用到了一个线程,线程代码如下:


private Handler handler = new Handler();
    private Runnable runnable = new Runnable() {
        public void run() {
            if (videoView.isPlaying()) {
                int current = videoView.getCurrentPosition();//获取播放过程中位置
                timeSeekBar.setProgress(current);//设置进度条的位置
                tvPlayTime.setText(time(videoView.getCurrentPosition()));//播放过程中的时间
            }
            handler.postDelayed(runnable, 500);//播放过程中0.5秒执行一次
        }
    };


然后是onCreate()方法


@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);//注释然后自动添加的
        timeSeekBar.setOnSeekBarChangeListener(onSeekBarChangeListener);//添加进度条的变化监听
        initVideo();//初始化VideoView
        //videoView播放完成监听
        videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                key = 1;//这是一个全局变量,用于控制遥控单击确定或者ok键时,是暂停继续还是重新播放,1则是重新播放视频
                btnRestartPlay.setVisibility(View.VISIBLE);//显示黑色背景,布局文件中注释提到了
                layFinishBg.setVisibility(View.VISIBLE);//显示白色重播图标,布局文件中注释提到了
            }
        });
    //videoView播放异常监听,类似于  此视频无法播放  这样的错误提示
        videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                Toast.makeText(MainActivity.this, "播放出错", Toast.LENGTH_SHORT).show();
                return false;
            }
        });
    }


代码中用到的一个方法:


/**
     * 时间转换方法
     * @param millionSeconds
     * @return
     */
    protected String time(long millionSeconds) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        Calendar c = Calendar.getInstance();
        c.setTimeInMillis(millionSeconds);
        return simpleDateFormat.format(c.getTime());
    }


控制视频是 播放还是暂停 或者是重播

/**
     * 控制视频是  播放还是暂停  或者是重播
     * @param isPlay
     * @param keys
     */
    private void isVideoPlay(boolean isPlay, int keys) {
        switch (keys) {
            case 0:
                if (isPlay) {//暂停
                    btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_player));//更换按钮的图标并显示出来
                    btnPlayOrPause.setVisibility(View.VISIBLE);
                    videoView.pause();
                } else {//继续播放
                    btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_pause));//更换按钮的图标并显示出来
                    btnPlayOrPause.setVisibility(View.VISIBLE);
                    // 开始线程,更新进度条的刻度
                    handler.postDelayed(runnable, 0);
                    videoView.start();//继续播放
                    timeSeekBar.setMax(videoView.getDuration());
                    timeGone();//当我们选择继续播放之后,就不能让这个图标一直显示下去,但是又不能马上消失,这样很突兀,所以用了延时1.5秒隐藏,比较合理,这个方法后面会贴出来。
                }
                break;
            case 1://重新播放
                initVideo();
                btnRestartPlay.setVisibility(View.GONE);//白色重播图标隐藏
                layFinishBg.setVisibility(View.GONE);//黑色背景隐藏
                key = 0;//重新播放之后,我们再将key置为0,这样就不会影响到下一次视频播放过程中的暂停和继续的监听操作了
                break;
        }


延时1.5秒隐藏


private void timeGone() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                btnPlayOrPause.setVisibility(View.INVISIBLE);
            }
        }, 1500);
    }


进度条监听


/**
     * 进度条监听
     */
    private SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
        // 当进度条停止修改的时候触发
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // 取得当前进度条的刻度
            int progress = seekBar.getProgress();
            if (videoView.isPlaying()) {
                // 设置当前播放的位置
                videoView.seekTo(progress);
            }
        }
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                                      boolean fromUser) {
        }
    };

时间转换方法


//将长度转换为时间
    StringBuilder mFormatBuilder = new StringBuilder();
    Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
  //整数类型转换为时分秒
    private String stringForTime(int timeMs) {
        int totalSeconds = timeMs / 1000;
        int seconds = totalSeconds % 60;
        int minutes = (totalSeconds / 60) % 60;
        int hours = totalSeconds / 3600;
        mFormatBuilder.setLength(0);
        if (hours > 0) {
            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
        } else {
            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
        }
    }


遥控器按键监听


private String TAG = "key";
    /**
     * 遥控器按键监听
     * @param keyCode
     * @param event
     * @return
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:     //确定键enter
            case KeyEvent.KEYCODE_DPAD_CENTER:
                Log.d(TAG, "enter--->");
                //如果是播放中则暂停、如果是暂停则继续播放
                isVideoPlay(videoView.isPlaying(), key);
                break;
            case KeyEvent.KEYCODE_BACK:    //返回键
                Log.d(TAG,"back--->");
                return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
            case KeyEvent.KEYCODE_SETTINGS: //设置键
                Log.d(TAG, "setting--->");
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:   //向下键
                /*    实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发
                 *    exp:KeyEvent.ACTION_UP
                 */
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    Log.d(TAG, "down--->");
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:   //向上键
                Log.d(TAG, "up--->");
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT: //向左键
                Log.d(TAG, "left--->");
                if (videoView.getCurrentPosition() > 4) {
                    videoView.seekTo(videoView.getCurrentPosition() - 5 * 1000);
                }
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:  //向右键
                Log.d(TAG, "right--->");
                videoView.seekTo(videoView.getCurrentPosition() + 5 * 1000);
                break;
            case KeyEvent.KEYCODE_VOLUME_UP:   //调大声音键
                Log.d(TAG, "voice up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键
                Log.d(TAG, "voice down--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音
                Log.d(TAG, "voice mute--->");
                break;
            default:
                break;
        }
        return super.onKeyDown(keyCode, event);
    }


前进后退的控制都这个遥控器监听里面。

MainActivity的完整代码如下:


package com.llw.androidtvdemo;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.llw.androidtvdemo.view.MyVideoView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Formatter;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity {
    @BindView(R.id.video_view)
    MyVideoView videoView;
    @BindView(R.id.tv_play_time)
    TextView tvPlayTime;
    @BindView(R.id.time_seekBar)
    SeekBar timeSeekBar;
    @BindView(R.id.tv_total_time)
    TextView tvTotalTime;
    @BindView(R.id.lay_finish_bg)
    RelativeLayout layFinishBg;
    @BindView(R.id.btn_play_or_pause)
    ImageButton btnPlayOrPause;
    @BindView(R.id.btn_restart_play)
    ImageButton btnRestartPlay;
    private int key = 0;
    private Handler handler = new Handler();
    private Runnable runnable = new Runnable() {
        public void run() {
            if (videoView.isPlaying()) {
                int current = videoView.getCurrentPosition();
                timeSeekBar.setProgress(current);
                tvPlayTime.setText(time(videoView.getCurrentPosition()));
            }
            handler.postDelayed(runnable, 500);
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        timeSeekBar.setOnSeekBarChangeListener(onSeekBarChangeListener);
        initVideo();
        videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                key = 1;
                btnRestartPlay.setVisibility(View.VISIBLE);
                layFinishBg.setVisibility(View.VISIBLE);
            }
        });
        videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                Toast.makeText(MainActivity.this, "播放出错", Toast.LENGTH_SHORT).show();
                return false;
            }
        });
    }
    /**
     * 时间转换方法
     * @param millionSeconds
     * @return
     */
    protected String time(long millionSeconds) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        Calendar c = Calendar.getInstance();
        c.setTimeInMillis(millionSeconds);
        return simpleDateFormat.format(c.getTime());
    }
    /**
     * 初始化VideoView
     */
    private void initVideo() {
        //本地视频
//        videoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/raw/test"));
        //网络视频
        final Uri uri = Uri.parse("http://gslb.miaopai.com/stream/ed5HCfnhovu3tyIQAiv60Q__.mp4");
        videoView.setVideoURI(uri);
        videoView.requestFocus();
        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                int totalTime = videoView.getDuration();//获取视频的总时长
                tvTotalTime.setText(stringForTime(totalTime));
                // 开始线程,更新进度条的刻度
                handler.postDelayed(runnable, 0);
                timeSeekBar.setMax(videoView.getDuration());
                //视频加载完成,准备好播放视频的回调
                videoView.start();
            }
        });
    }
    /**
     * 控制视频是  播放还是暂停  或者是重播
     * @param isPlay
     * @param keys
     */
    private void isVideoPlay(boolean isPlay, int keys) {
        switch (keys) {
            case 0:
                if (isPlay) {//暂停
                    btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_player));
                    btnPlayOrPause.setVisibility(View.VISIBLE);
                    videoView.pause();
                } else {//继续播放
                    btnPlayOrPause.setBackground(getResources().getDrawable(R.mipmap.icon_pause));
                    btnPlayOrPause.setVisibility(View.VISIBLE);
                    // 开始线程,更新进度条的刻度
                    handler.postDelayed(runnable, 0);
                    videoView.start();
                    timeSeekBar.setMax(videoView.getDuration());
                    timeGone();
                }
                break;
            case 1://重新播放
                initVideo();
                btnRestartPlay.setVisibility(View.GONE);
                layFinishBg.setVisibility(View.GONE);
                key = 0;
                break;
        }
    }
    /**
     * 延时隐藏
     */
    private void timeGone() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                btnPlayOrPause.setVisibility(View.INVISIBLE);
            }
        }, 1500);
    }
    /**
     * 进度条监听
     */
    private SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
        // 当进度条停止修改的时候触发
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // 取得当前进度条的刻度
            int progress = seekBar.getProgress();
            if (videoView.isPlaying()) {
                // 设置当前播放的位置
                videoView.seekTo(progress);
            }
        }
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress,
                                      boolean fromUser) {
        }
    };
    //将长度转换为时间
    StringBuilder mFormatBuilder = new StringBuilder();
    Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
    private String stringForTime(int timeMs) {
        int totalSeconds = timeMs / 1000;
        int seconds = totalSeconds % 60;
        int minutes = (totalSeconds / 60) % 60;
        int hours = totalSeconds / 3600;
        mFormatBuilder.setLength(0);
        if (hours > 0) {
            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
        } else {
            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
        }
    }
    private String TAG = "key";
    /**
     * 遥控器按键监听
     * @param keyCode
     * @param event
     * @return
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:     //确定键enter
            case KeyEvent.KEYCODE_DPAD_CENTER:
                Log.d(TAG, "enter--->");
                //如果是播放中则暂停、如果是暂停则继续播放
                isVideoPlay(videoView.isPlaying(), key);
                break;
            case KeyEvent.KEYCODE_BACK:    //返回键
                Log.d(TAG,"back--->");
                return true;   //这里由于break会退出,所以我们自己要处理掉 不返回上一层
            case KeyEvent.KEYCODE_SETTINGS: //设置键
                Log.d(TAG, "setting--->");
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:   //向下键
                /*    实际开发中有时候会触发两次,所以要判断一下按下时触发 ,松开按键时不触发
                 *    exp:KeyEvent.ACTION_UP
                 */
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    Log.d(TAG, "down--->");
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:   //向上键
                Log.d(TAG, "up--->");
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT: //向左键
                Log.d(TAG, "left--->");
                if (videoView.getCurrentPosition() > 4) {
                    videoView.seekTo(videoView.getCurrentPosition() - 5 * 1000);
                }
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:  //向右键
                Log.d(TAG, "right--->");
                videoView.seekTo(videoView.getCurrentPosition() + 5 * 1000);
                break;
            case KeyEvent.KEYCODE_VOLUME_UP:   //调大声音键
                Log.d(TAG, "voice up--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN: //降低声音键
                Log.d(TAG, "voice down--->");
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE: //禁用声音
                Log.d(TAG, "voice mute--->");
                break;
            default:
                break;
        }
        return super.onKeyDown(keyCode, event);
    }
}


运行效果如下:


20200310113427242.png


20200310113455884.png


播放结束再按确定键就可以重新播放了。


这里再补充一下,在我之前使用的机顶盒很少使用高版本的Android系统,所以没有对高版本的Android系统的https网络访问许可做支持,在Android9.0及以后的版本中,默认是不支持https请求的,这就导致了有一些读者在使用源码是会出现 “ 无法播放此视频 ”,这样的bug,因此我这里补充一下,其实也很简单,在res下新建一个xml文件夹,xml文件夹下新建一个network_security_config.xml文件


3000d304914c494db89c6d23a140b4d7.png


xml文件内容如下:


<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>


然后在AndroidManifest.xml的application标签里配置就可以了。


683d37236cc14489810ca5a31ea788f8.png

如有问题请留言,定当第一时间回复您,感谢您的阅读,我是初学者-Study 山高水长,后会有期~

相关文章
|
21天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
46 19
|
21天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
49 14
|
24天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
22天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
32 5
|
21天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
22天前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
22天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
25天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
22天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
30 0
|
25天前
|
存储 监控 Java
探索安卓开发:从基础到进阶的旅程
在这个数字时代,移动应用已成为我们日常生活的一部分。对于开发者来说,掌握安卓开发不仅是技能的提升,更是通往创新世界的钥匙。本文将带你了解安卓开发的核心概念,从搭建开发环境到实现复杂功能,逐步深入安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的见解和技巧,帮助你在安卓开发的道路上更进一步。
22 0