概述

之前在公司都是做金融和博彩相关的app,很少接触到视频播放类的应用开发,趁最近比较闲,想逐步学习相关的知识,给自己增加点视频类app开发的经验,也希望读者能够从我个人的学习当中得到一些启发。

一、学习MediaPlayer的API

对于我来讲,学习新东西第一步就是看相关的api,于是我找到了Android中最基本的MediaPlayer的开发文档进行简单的学习。


从图我们可以看出,MediaPlayer是个单独的子类,继承Object,说明他就是最基本的实现类,学习起来就相对容易。

紧接着,api中列出了MediaPlayer的运作周期的状态图,如下图所示:

其中,蓝色椭圆内是代表MediaPlayer的某个周期状态,单箭头表示同步,双箭头表示异步,弧形表示程序执行期间的调用,或者说在某个类执行期间调用某些方法。

从上图中我们能看到MediaPlayer类中有以下一些状态:

  • Idle

    • 当一个MediaPlayer类通过new实例化或者执行了reset()方法之后,就会进入到Idle状态。

    • API中提到,如果在Idle状态下调用诸如 getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() or prepareAsync()等等获取媒体文件属性等方法时,会报错,并回调OnErrorListener.OnError()方法。

  • End

    • 当状态在Idle时,若调用release()方法后,状态将变为End结束。

    • 该状态会释放一切播放器所持有的资源,结束所有回调和引用的实例,进入此状态后,将不能回到其他的状态中去。

  • Error

    • 如果在MediaPlayer刚被构建出来的时候,就调用获取媒体文件属性等方法时,播放器内部将不会执行用户所构造出来的类似OnErrorListener.onError()这类回调,并且状态也不会变为Error。但是如果用户是在reset()方法执行之后再调用那些方法时,就会回调监听方法,并且状态变为Error。

    • 一旦播放器在播放过程中发生错误,即使开发者没有注册相关错误回调方法,播放器也会变成Error状态。
      若在Error中调用reset()方法,将会回到Idle状态。

    • API也建议我们注册一个错误监听器能更有效的监听播放器在运行过程中的错误状态原因,并随时修正我们的代码。

  • Initialized

    • 当调用setDateSource()方法后,播放器当前状态将从Idle装变为Initialized

    • 如果setDateSource()在其他状态下被调用,将会报错

  • Prepared

  • Preparing

    • 如果我们要播放某个媒体文件,在播放器进入Started状态之前,需要在Initialized状态调用prepare()方法让播放器进入Prepared(准备)状态。

    • 有两种方式(同步/异步)可以到达Prepared状态。一种是直接执行prepare()方法,当方法返回后,就将进入Prepared状态,也称之为同步的方法。如果是异步的方法,可以调用prepareAsync()方法让播放引擎进入准备中的状态。当准备完成或prepare()方法返回后,播放引擎就会回调开发者所注册的回调接口setOnPreparedListener()。

    • 需要注意的是,Preparing的状态是一个过渡状态,改状态的时候,一些回调方法将会出现不会执行的情况

    • 在Prepared状态时,可以设置一些播放器配置方法。

  • Started

    • 在执行start()方法后,播放器进入started状态,该状态时,播放器将会开始播放,isPlaying()方法将会调用,也会告诉你是否已经在Started状态中。

    • 如果开发者注册了setOnBufferingUpdateListener(OnBufferingUpdateListener)监听,那么在这个状态时成功后,会回调该监听。该接口可以监听媒体流的状态。

  • Stopped

  • Paused

    • 当调用stop()方法或者pause()方法后,相应的,播放器将会进入Stopped和Paused状态。注意的是,这个转变状态的过程是异步过程,所以在改变状态的时候不是立即转变,而是需要一定时间的延时。

    • 如果播放引擎处于Paused状态,这时调用start()方法,播放引擎将会重新回到started状态,并开始从暂停出开始播放。

    • 如果调用stop()方法,那么如果播放器处于Started, Paused, Prepared or PlaybackCompleted 状态时,都将转化成Stopped状态,并且播放器将不能继续播放,除非让播放器重新回到Prepared状态后,才能开启播放。

  • PlaybackCompleted

    • 重放可以通过seekTo(int)方法进行设置,当然该方法其实也属于异步方法,在调用完成后,播放引擎会回调OnSeekComplete.onSeekComplete()接口方法来告知开发者重放设置完成。getCurrentPosition()可以获取重放的真实位置。图中Prepared, Paused,started和PlaybackCompleted 这些状态都可以调用seekTo()方法进行重放。

    • 当媒体流文件到最后时,播放将会完成并结束,如果回放模式被设置setLooping(boolean),那么播放器将保持started状态。如果没有设置回放,则将调用OnCompletion.onCompletion()方法,也会调用开发者注册的接口setOnCompletionListener(OnCompletionListener),并进入PlaybackCompleted状态。

    • 在该状态下,如果调用start()方法,则会重置播放器资源到初始状态,并回到started状态。

二、常用API方法

简单的介绍完API给我们展示的状态模型之后,我给大家列出部分常用的方法,其他的方法请大家自行查阅开发者文档。

  • getCurrentPosition( ):得到当前的播放位置
  • getDuration() :得到文件的时间
  • getVideoHeight() :得到视频高度
  • getVideoWidth() :得到视频宽度
  • isLooping():是否循环播放
  • isPlaying():是否正在播放
  • pause():暂停
  • prepare():准备(同步)
  • prepareAsync():准备(异步)
  • release():释放MediaPlayer对象
  • reset():重置MediaPlayer对象
  • seekTo(int msec):指定播放的位置(以毫秒为单位的时间)
  • setAudioStreamType(int streamtype):指定流媒体的类型
  • setDisplay(SurfaceHolder sh):设置用SurfaceHolder来显示多媒体
  • setLooping(boolean looping):设置是否循环播放
  • setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener): 网络流媒体的缓冲监听
  • setOnCompletionListener(MediaPlayer.OnCompletionListener listener): 网络流媒体播放结束监听
  • setOnErrorListener(MediaPlayer.OnErrorListener listener): 设置错误信息监听
  • setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener): 视频尺寸监听
  • setScreenOnWhilePlaying(boolean screenOn):设置是否使用SurfaceHolder显示
  • setVolume(float leftVolume, float rightVolume):设置音量
  • start():开始播放
  • stop():停止播放

以上是部分比较常用的方法及监听接口,接下来我将写一个小项目来运用这些方法和接口。

三、制作简单的视频播放器

或许你的公司会开始做一个视频项目,那么现在有个需求是这样的:封装一个简单的视频播放器,当然这个播放器的样式是可以自定义的,基本功能满足市面上其他视频类APP的播放功能。好,那么现在我们开始调研视频类APP的产品,并设计相应的功能。

1)设计思路

以爱奇艺视频播放应用为例,我模拟了一个点进某个视频,进入播放详情页的情景。

从上图我们可以看到,此播放器有4个功能,分别为播放、暂停、调节播放进度、全屏,那么我先实现最简单的播放暂停和进度选取的功能。

通过对图表的分析,我们知道,播放器的每一个状态在变换之前,都只能根据图上箭头所表明的状态进行变更,例如:started状态之后,你可以变成paused状态或者stopped状态,但是,如果程序已经为stopped状态,这时候你调用pause()方法,那么程序就会报错,甚至崩溃。所以在封装的时候,需要弄清楚播放器当前是处于什么样的状态。

我们知道surfaceview的绘制是在子线程中绘制完成的,所以诸如视频 、游戏这类开发都会用到surfaceview。所以在开发的过程中,我们可以把视频有关的功能一起和surfaceview进行统一封装,留出一些方法进行调用,以免引起阻塞主线程的操作(播放进度条的视图刷新),造成卡顿。网上有一些框架就是这么做的。

这里就针对上面几个功能贴上代码。代码里没有将功能和surfaceview一起封装,仅仅作为参考。

最终效果图如图所示:

这里除了上述3个功能外,加入了”停止播放”功能和“装碟”功能,意思就是需要先装载媒体文件,再进行播放,模拟了网络异步加载的情景。

首先我写了几个接口,用于回调改变view的状态。

public interface MediaControl {
/*** 开始播放*/
void startPlayer();/*** 停止播放*/
void stopPlayer();/*** 暂停播放*/
void pausePlayer();/*** 播放总长度* @param value 时间显示 例如 03:59* @param val   当前时间毫秒数*/
void totalLengthSecond(String value,int val);

}

这里发下我自定义surfaceview的写法吧

package com.example.mediaplayertest;import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;import java.io.IOException;public class MediaSurfaceView extends SurfaceView implements SurfaceHolder.Callback, MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {public static String TAG = "Media";
private MediaPlayer mPlayer;
private MediaControl viewInterface;//未加载媒体文件状态
private int STATE_UNLOADING = 0x000;
//准备状态
private int STATE_PREPARED = 0x001;
//暂停状态
private int STATE_PAUSED = 0x002;
//停止状态
private int STATE_STOPPED = 0x003;
//开始状态
private int STATE_STARTED = 0x004;
//当前状态
private int currentState = STATE_UNLOADING;public MediaSurfaceView(Context context, AttributeSet attrs) {this(context, attrs, -1);
}public MediaSurfaceView(Context context) {this(context, null);
}public MediaSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);initial();
}private void initial() {loadingResource();getHolder().addCallback(this);getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}private void loadingResource() {if (mPlayer == null) {mPlayer = new MediaPlayer();mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mPlayer.setOnErrorListener(this);mPlayer.setOnPreparedListener(this);mPlayer.setOnCompletionListener(this);}
}@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {mPlayer.setDisplay(surfaceHolder);
}@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {Log.d(TAG, "surfaceChanged");
}@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {Log.d(TAG, "surfaceDestroyed");ifStart = false;if (mPlayer == null) return;if (mPlayer.isPlaying()) {mPlayer.stop();}mPlayer.release();
}/*** 开始播放*/
public void startPlayer() {if (STATE_PREPARED != currentState &&STATE_PAUSED != currentState) return;mPlayer.start();if (viewInterface != null) {viewInterface.startPlayer();}currentState = STATE_STARTED;
}/*** 停止播放*/
public void stopPlayer() {if (STATE_UNLOADING == currentState ||STATE_STOPPED == currentState) return;mPlayer.stop();mPlayer.prepareAsync();if (viewInterface != null) {viewInterface.stopPlayer();}currentState = STATE_STOPPED;
}/*** 暂停播放*/
public void pausePlayer() {if (STATE_STARTED != currentState) return;mPlayer.pause();if (viewInterface != null) {viewInterface.pausePlayer();}currentState = STATE_PAUSED;
}/*** 装载*/
public void loadingPlayer() {try {AssetFileDescriptor fileDescriptor = getContext().getAssets().openFd("gaobaiqiqiu.mp4");mPlayer.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(),fileDescriptor.getLength());mPlayer.prepareAsync();} catch (IOException e) {e.printStackTrace();}
}public void setViewInterface(MediaControl viewInterface) {this.viewInterface = viewInterface;
}/*** 播放器错误状态监听** @param mediaPlayer* @param what* @param extra* @return*/
@Override
public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {if (MediaPlayer.MEDIA_ERROR_UNKNOWN == what) {//未知错误Log.d(TAG, "onError what    未知错误");} else if (MediaPlayer.MEDIA_ERROR_SERVER_DIED == what) {//媒体服务中断,应用必须释放类新初始化Log.d(TAG, "onError what    媒体崩溃");} else {Log.d(TAG, "onError what    其他错误");}switch (extra) {case MediaPlayer.MEDIA_ERROR_IO:Log.d(TAG, "onError extra    文件或网络关联错误");break;case MediaPlayer.MEDIA_ERROR_MALFORMED:Log.d(TAG, "onError extra    比特流未遵守相关编码标准或者文件细则");break;case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:Log.d(TAG, "onError extra    比特流未遵守相关编码标准或者文件细则");break;case MediaPlayer.MEDIA_ERROR_TIMED_OUT:Log.d(TAG, "onError extra    超时");break;default:Log.d(TAG, "onError extra    其他错误");break;}return false;
}/*** 播放器装在准备** @param mediaPlayer*/
@Override
public void onPrepared(MediaPlayer mediaPlayer) {currentState = STATE_PREPARED;int totalLength = mPlayer.getDuration();if (viewInterface != null) {viewInterface.totalLengthSecond(formatTime(totalLength),totalLength);}
}/*** 格式化时间,将毫秒转换为分:秒格式** @param time* @return*/
public static String formatTime(long time) {if (time < 0) return "-1";String min = time / (1000 * 60) + "";String sec = time % (1000 * 60) + "";if (min.length() < 2) {min = "0" + time / (1000 * 60) + "";} else {min = time / (1000 * 60) + "";}if (sec.length() == 4) {sec = "0" + (time % (1000 * 60)) + "";} else if (sec.length() == 3) {sec = "00" + (time % (1000 * 60)) + "";} else if (sec.length() == 2) {sec = "000" + (time % (1000 * 60)) + "";} else if (sec.length() == 1) {sec = "0000" + (time % (1000 * 60)) + "";}return min + ":" + sec.trim().substring(0, 2);
}@Override
public void onCompletion(MediaPlayer mediaPlayer) {stopPlayer();
}/*** 是否开启循环线程*/
private boolean ifStart = false;private Thread ProgressThread = new Thread() {private Bundle mBundle = new Bundle();@Overridepublic void run() {super.run();while (ifStart && handler != null) {if (mPlayer != null && mPlayer.isPlaying()) {Message message = new Message();message.what = 0x00;//当前格式化后的时间mBundle.putString("TIME", formatTime(mPlayer.getCurrentPosition()));//当前毫秒int curtime = mPlayer.getCurrentPosition();message.setData(mBundle);mBundle.putInt("CURRENT_TIME",curtime);handler.sendMessage(message);}try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();ifStart = false;}}}
};private Handler handler;public void setHandler(Handler handler) {this.handler = handler;ifStart = true;ProgressThread.start();
}/*** 进度调至某个时间点后继续播放* @param progress*/
public void seekTo(int progress) {if(mPlayer!=null){if(STATE_PREPARED==currentState ||STATE_STARTED == currentState||STATE_PAUSED  == currentState){mPlayer.seekTo(progress);}}
}
}

我将几个状态做了限制,规定了在什么状态下才能执行那种功能。比如“停止”之后就不能“暂停”。以免报错。进度条我用了线程来实时获取当前进度,通过handler进行线程间的通信,实现UI的进度条更新。
接下来是主界面代码:

package com.example.mediaplayertest;import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;import myview.LoadingView;public class MediaActivity extends Activity implements View.OnClickListener,MediaControl, SeekBar.OnSeekBarChangeListener {
private LoadingView mLoadingView;private MediaSurfaceView mSurfaceView;
private LinearLayout LLloading;private Button btnStart;
private Button btnStop;
private Button btnPause;
private Button btnLoading;private TextView currentTime;
private TextView totalTime;
private SeekBar mSeekBar;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.mediaplayer_layout);mLoadingView = (LoadingView) findViewById(R.id.loadingView);mLoadingView.animationOpen();mSurfaceView = (MediaSurfaceView) findViewById(R.id.media_player_sv);mSurfaceView.setViewInterface(this);mSurfaceView.setHandler(mHandler);LLloading = (LinearLayout) findViewById(R.id.media_loading);btnStart = (Button) findViewById(R.id.btn_start);btnStop = (Button) findViewById(R.id.btn_stop);btnPause = (Button) findViewById(R.id.btn_pause);btnLoading = (Button) findViewById(R.id.btn_loading);btnStart.setOnClickListener(this);btnStop.setOnClickListener(this);btnPause.setOnClickListener(this);btnLoading.setOnClickListener(this);currentTime = (TextView) findViewById(R.id.current_time);totalTime = (TextView) findViewById(R.id.total_time);mSeekBar = (SeekBar) findViewById(R.id.my_seekBar);mSeekBar.setOnSeekBarChangeListener(this);
}private Handler mHandler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch(msg.what){case 0x00:Bundle mBundle  = msg.getData();String curTime = (String) mBundle.get("TIME");int intCurTime = (int) mBundle.get("CURRENT_TIME");mSeekBar.setProgress(intCurTime);currentTime.setText(curTime);break;}}
};@Override
public void onClick(View view) {switch (view.getId()){case  R.id.btn_start:mSurfaceView.startPlayer();break;case R.id.btn_stop:mSurfaceView.stopPlayer();break;case R.id.btn_pause:mSurfaceView.pausePlayer();break;case R.id.btn_loading:mSurfaceView.loadingPlayer();break;}
}@Override
public void startPlayer() {mSurfaceView.setVisibility(View.VISIBLE);LLloading.setVisibility(View.INVISIBLE);mLoadingView.animationClose();
}@Override
public void stopPlayer() {}@Override
public void pausePlayer() {}@Override
public void totalLengthSecond(String value,int val) {totalTime.setText(value);mSeekBar.setMax(val);
}@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if(progress>0){if(fromUser){mSurfaceView.seekTo(progress);}}
}@Override
public void onStartTrackingTouch(SeekBar seekBar) {}@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
}

代码中主要实现界面的改变。布局文件这里就不在贴 了,主要就是surfaceview上方叠了一层布局来显示loading时候的画面,当播放时,将loading布局隐藏后就能显示播放的画面了。

当然,图中的状态循环还涉及到回放等功能,因为用得不多,所以这里就不再赘述了。如果想自己做类似的视频框架的话,可以再进一步的封装,比如在surfaceview中加入“切换全屏”的功能,根据手势调整声音或者屏幕亮度的功能。

制作视频播放器功能,首先保证视频能够正常播放,并且不影响UI线程的运作;第二就是媒体文件播放的时候,屏幕适配的问题;第三就是视频的解码等等可能涉及底层的知识。今后在工作中继续学习吧。

Android视频播放器开发—— 探究MediaPlayer相关推荐

  1. 基于NDK、C++、FFmpeg的android视频播放器开发实战-夏曹俊-专题视频课程

    基于NDK.C++.FFmpeg的android视频播放器开发实战-1796人已学习 课程介绍         课程包含了对流媒体(拉流)的播放,演示了播放rtmp的香港卫视,支持rtsp摄像头和ht ...

  2. 视频教程-基于NDK、C++、FFmpeg的android视频播放器开发实战-Android

    基于NDK.C++.FFmpeg的android视频播放器开发实战 夏曹俊:南京捷帝科技有限公司创始人,南京大学计算机硕士毕业,有15年c++跨平台项目研发的经验,领导开发过大量的c++虚拟仿真,计算 ...

  3. 记一次Android视频播放器开发

    播放器入门 看小电影多年 当年甚至还是用QTplayer:而李开复,已经从技术员,技术总监,HR,出书,风投,隐退的华丽转身 而我们还只是个"程序员" 这么多年过去了,好像还真没认 ...

  4. csdn android视频播放器开发

    http://blog.csdn.net/column/details/myvideo.html

  5. 学习笔记(2):基于NDK、C++、FFmpeg的android视频播放器开发实战-音视频基础知识Mpeg4封装格式音视频编码格式讲解...

    立即学习:https://edu.csdn.net/course/play/7417/151027?utm_source=blogtoedu 封装:将音视频从文件中读出来 解码:解压出来,转换成显卡支 ...

  6. Android进阶:自定义视频播放器开发(上)

    随着快手,抖音,西瓜视频等视频APP的崛起,视频播放已经成为主流,此时作为Android研发的你,想要提高自己的能力还不知道怎么开发视频播放器怎么行?所以今天就带着大家一起开发一个简易播放器:Smal ...

  7. 实现在Android本地视频播放器开发

    在Android本地视频播放器开发中的搜索本地视频章节中,我们能够搜索本地视频并且显示每个视频的图片.标题.时间长度,当然如果需要添加其他的例如视频的长度和宽度可以使用Video类中的方法,既然我们获 ...

  8. Android本地视频播放器开发--视频解码

    在上一章Android本地视频播放器开发--SDL编译编译中编译出sdl的支持库,当时我们使用的2.0,但是有些api被更改了,所以在以下的使用者中我们使用SDL1.3的库,这个库我会传上源码以及编译 ...

  9. Android进阶:自定义视频播放器开发(下)

    上一篇文章我们主要讲了视频播放器开发之前需要准备的一个知识,TextureView,用于对图像流的处理.这篇文章开始构建一个基础的视频播放器. 一.准备工作 在之前的文章已经说过了,播放器也是一个vi ...

最新文章

  1. 全网仅此一篇:工业级压力传感器设计及实现(华大半导体HC32L136)
  2. 如何扩容单台服务器的存储容量?
  3. 微信小游戏开发教程-2D游戏原理讲解
  4. HardwareSoftwareTutorial
  5. 【深度学习】6万字解决算法面试中的深度学习基础问题
  6. Redis 是如何执行的?
  7. KVM之Live Migration
  8. Year-End Review
  9. 【C/C++】最大公约数和最小公倍数(辗转相除、更相减损、stein)
  10. 20145324 20145325《信息安全系统设计基础》实验二 固件设计
  11. 《狂人C》阅读笔记(1)
  12. ByteBuf详解和Netty中的拆包粘包原理解析
  13. 同事篇(12年至今)
  14. 中文分词技术--统计分词
  15. UE4--局域网多人联机
  16. java文件上传后台
  17. 考研还是工作?回过头来反思我当初为何没考研
  18. 微信小程序wx:key使用
  19. 跨境电商独立站怎么去搭建
  20. 低依赖C++ GUI库imgui笔记

热门文章

  1. comsol-空气域的电导率
  2. 谷歌互联网气球开始测试 面向巴西地区
  3. 中兴通讯股份有限公司
  4. 75亿美元!欧盟批准微软收购Zenimax,Xbox获史诗级加强
  5. 计算机毕业设计JavaVue.js网上书城管理系统设计与实现服务端(源码+系统+mysql数据库+lw文档)
  6. 计算机考试怎么把要求栏固定到屏幕上,计算机作业1.调整任务栏的位置至屏幕上方2.使任务栏上不显示 爱问知识人...
  7. Swim-Transform V2:用于目标检测,视觉大模型不再是难题(附源代码)
  8. Go 通过 cobra 快速构建命令行应用
  9. 关系的无损链接、函数依赖的判断
  10. android 8 不更新,[已解决]您可能遇到的Android 8 Oreo更新问题