MediaPlayer的生命周期
了解播放器的生命周期非常重要,因为不合法的状态下调用一些方法,如prepare(),prepareAsync()和setDataSource()方法会抛出IllegalStateException异常。使用ijkplayer播放同样如此,可能个别位置略有不同
因此首先要知道这两点
- 当一个MediaPlayer对象被刚刚用new操作符创建或是调用了reset()方法后,它就处于Idle状态。
- 当调用了release()方法后,它就处于End状态。这两种状态之间是MediaPlayer对象的生命周期。
注意点:
-
在处于Idle状态时,调用getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() 或者 prepareAsync() 方法都会报错。
-
由于种种原因一些播放控制操作可能会失败,如不支持的音频/视频格式,缺少隔行扫描的音频/视频,分辨率太高,流超时等原因,等等。因此,错误报告和恢复在这种情况下是非常重要的。
- MediaPlayer对象会进入到Error状态
- 为了重用一个处于Error状态的MediaPlayer对象,可以调用reset()方法来把这个对象恢复成Idle状态,而在ijikplayer中需要重新new一个播放器对象
-
调用setDataSource(FileDescriptor)方法,或setDataSource(String)方法,或setDataSource(Context,Uri)方法,或setDataSource(FileDescriptor,long,long)方法会使处于Idle状态的对象迁移到Initialized状态。
- 若当此MediaPlayer处于其它的状态下,调用setDataSource()方法,会抛出IllegalStateException异常
-
在开始播放之前,MediaPlayer对象必须要进入Prepared状态。
有两种方法(同步和异步)可以使MediaPlayer对象进入Prepared状态:
- 调用prepare()方法,此方法返回就表示该MediaPlayer对象已经进入了Prepared状态;
- 调用prepareAsync()方法,此方法会使此MediaPlayer对象进入Preparing状态并返回,而内部的播放引擎会继续未完成的准备工作。当同步版本返回时或异步版本的准备工作完全完成时就会调用客户端程序员提供的OnPreparedListener.onPrepared()监听方法。可以调用MediaPlayer.setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)方法来注册OnPreparedListener.
- MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等
-
要开始播放,必须调用start()方法。当此方法成功返回时,MediaPlayer的对象处于Started状态。 isPlaying()方法可以被调用来测试某个MediaPlayer对象是否在Started状态。
- 当处于Started状态时,内部播放引擎会调用客户端程序员提供的OnBufferingUpdateListener.onBufferingUpdate()回调方法,此回调方法允许应用程序追踪流播放的缓冲的状态。
- 播放可以被暂停,停止,以及调整当前播放位置。当调用pause()方法并返回时,会使MediaPlayer对象进入Paused状态。
注意Started与Paused状态的相互转换在内部的播放引擎中是异步的。所以可能需要一点时间在isPlaying()方法中更新状态,若在播放流内容,这段时间可能会有几秒钟。因此用自己定义的isPlaying做状态值较好
调用start()方法会让一个处于Paused状态的MediaPlayer对象从之前暂停的地方恢复播放。
已经处于Paused状态的MediaPlayer对象pause()方法没有影响。
调用stop()方法会停止播放,并且还会让一个处于Started,Paused,Prepared或PlaybackCompleted状态的MediaPlayer进入Stopped状态。
- 当播放到流的末尾,播放就完成了
若没有开启循环模式,那么内部的播放引擎会调用客户端程序员提供的OnCompletion.onCompletion()回调方法。可以通过调用MediaPlayer.setOnCompletionListener(OnCompletionListener)方法来设置。内部的播放引擎一旦调用了OnCompletion.onCompletion()回调方法,说明这个MediaPlayer对象进入了PlaybackCompleted状态。
因此为了好用,而不会随便抛出IllegalStateException,一般会自己进一步封装一个MediaPlayer
MediaPlayer初始化
mediaPlayer=new MediaPlayer();
try{
mediaPlayer.setDataSource(context, Uri.parse(videoPath));
}catch (IOException e){
e.printStackTrace();
}
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setLooping(true);
输出MediaPlayer的数据到SurfaceTexture
创建一个纹理
MediaPlayer的输出往往不是RGB格式(一般是YUV),而GLSurfaceView需要RGB格式才能正常显示,另外,获取每一帧的数据并没有那么方便。
SurfaceTexture的主要作用就是,从视频流和相机数据流获取新一帧的数据,获取新数据调用的方法是updateTexImage
和用SurfaceView做视频播放的区别就在于:这个输出可以不显示出来,取决于你的render逻辑,这样在处理视频的时候就更自由一些,完成一些特殊需求
创建SurfaceTexture
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
textureId = textures[0];
//我们用GLES11Ext.GL_TEXTURE_EXTERNAL_OES来代替了GLES20.GL_TEXTURE_2D
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
ShaderUtils.checkGlError("glBindTexture mTextureID");
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES11Ext.GL_TEXTURE_EXTERNAL_OES的用处是什么?
视频解码的输出格式是YUV的(YUV420sp),那么这个扩展纹理的作用就是实现YUV格式到RGB的自动转化,我们就不需要再为此写YUV转RGB的代码了
在onSurfaceCreated的最后加上如下代码:
surfaceTexture = new SurfaceTexture(textureId);
//监听是否有新的一帧数据到来,我们设置为this,是让GLRenderer 来实现这个接口
surfaceTexture.setOnFrameAvailableListener(this);
Surface surface = new Surface(surfaceTexture);
mediaPlayer.setSurface(surface);
surface.release();
public class GLRenderer implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener
@Override
synchronized public void onFrameAvailable(SurfaceTexture surface) {
updateSurface = true;
}
synchronized (this){
if (updateSurface){
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(mSTMatrix);
updateSurface = false;
}
}
有新数据时,用updateTexImage来更新纹理,这个getTransformMatrix的目的,是让新的纹理和纹理坐标系能够正确的对应,mSTMatrix的定义是和projectionMatrix完全一样的。
private float[] mSTMatrix = new float[16];
更新着色器代码
使用了视频作为输入源,需要更新着色器代码
fragment_shader.glsl
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTexCoord;
uniform samplerExternalOES sTexture;
void main() {
gl_FragColor=texture2D(sTexture, vTexCoord);
}
我们使用samplerExternalOES代替之前的sampler2D,和surfaceTexture配合进行纹理更新和格式转换,第一行的注解必须加上
vertex_shader.glsl
attribute vec4 aPosition;
attribute vec4 aTexCoord;
varying vec2 vTexCoord;
uniform mat4 uMatrix;
uniform mat4 uSTMatrix;
void main() {
vTexCoord = (uSTMatrix * aTexCoord).xy;
gl_Position = uMatrix*aPosition;
}
在顶点着色器中,需要加入对应的uSTMatrix,并且aTexCoord要改成长度为4的向量,以便于做乘法操作。
更新GLRenderer
在GLRenderer中,用类似的方法获取uSTMMatrixHandle
uSTMMatrixHandle = GLES20.glGetUniformLocation(programId, "uSTMatrix");
每次绘制的时候,将mSTMatrix用类似的方法传给OpenGL:
GLES20.glUniformMatrix4fv(uSTMMatrixHandle, 1, false, mSTMatrix, 0);
因为用了扩展纹理,所以我们绑定的纹理类型也要做修改:
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,textureId);
我们刚才只是设定了一个boolean来表示可以更新纹理了,却没有具体操作,所以在onDrawFrame最开始加入如下代码
synchronized (this){
if (updateSurface){
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(mSTMatrix);
updateSurface = false;
}
}
然后我们就可以试一下指定一个视频源,看看能不能正常显示(别忘了声明权限哦)
视频颠倒的问题
其实,不要更新mSTMatrix,将他设为单位阵,一般就会显示正常的视频。。
在使用mSTMatrix的情况下,解决方法就是修改顶点数组或者修改纹理数组,我们采用修改顶点数组的方案:
private final float[] vertexData = {
1f,-1f,0f,
-1f,-1f,0f,
1f,1f,0f,
-1f,1f,0f
};
屏幕尺寸自适应
正交投影!
屏幕尺寸是在onSurfaceCreated获取的。
获取视频尺寸,获取视频的尺寸可以设置一个监听器:
mediaPlayer.setOnVideoSizeChangedListener(this);
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
Log.d(TAG, "onVideoSizeChanged: "+width+" "+height);
updateProjection(width,height);
}
有了这两个尺寸,我们就可以更新Projection矩阵了,代码可能有些难以理解,对于videoRatio>screenRatio,其实就是我们是将videoHeight/videoWidth当做一个单位来处理(因为之前的坐标映射),然后我们用screenHeight/screenWidth再去除这个单位,来获得屏幕Y轴方向应该有的范围
private void updateProjection(int videoWidth, int videoHeight){
float screenRatio=(float)screenWidth/screenHeight;
float videoRatio=(float)videoWidth/videoHeight;
if (videoRatio>screenRatio){
Matrix.orthoM(projectionMatrix,0,-1f,1f,-videoRatio/screenRatio,videoRatio/screenRatio,-1f,1f);
}else Matrix.orthoM(projectionMatrix,0,-screenRatio/videoRatio,screenRatio/videoRatio,-1f,1f,-1f,1f);
}