通过OpenGL和MediaPlayer播放视频

MediaPlayer的生命周期

了解播放器的生命周期非常重要,因为不合法的状态下调用一些方法,如prepare(),prepareAsync()和setDataSource()方法会抛出IllegalStateException异常。使用ijkplayer播放同样如此,可能个别位置略有不同

因此首先要知道这两点

  • 当一个MediaPlayer对象被刚刚用new操作符创建或是调用了reset()方法后,它就处于Idle状态。
  • 当调用了release()方法后,它就处于End状态。这两种状态之间是MediaPlayer对象的生命周期。

注意点:

  1. 在处于Idle状态时,调用getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() 或者 prepareAsync() 方法都会报错。
  2. 由于种种原因一些播放控制操作可能会失败,如不支持的音频/视频格式,缺少隔行扫描的音频/视频,分辨率太高,流超时等原因,等等。因此,错误报告和恢复在这种情况下是非常重要的。
  • MediaPlayer对象会进入到Error状态
  • 为了重用一个处于Error状态的MediaPlayer对象,可以调用reset()方法来把这个对象恢复成Idle状态,而在ijikplayer中需要重新new一个播放器对象
  1. 调用setDataSource(FileDescriptor)方法,或setDataSource(String)方法,或setDataSource(Context,Uri)方法,或setDataSource(FileDescriptor,long,long)方法会使处于Idle状态的对象迁移到Initialized状态。
  • 若当此MediaPlayer处于其它的状态下,调用setDataSource()方法,会抛出IllegalStateException异常
  1. 在开始播放之前,MediaPlayer对象必须要进入Prepared状态。

有两种方法(同步和异步)可以使MediaPlayer对象进入Prepared状态:

  • 调用prepare()方法,此方法返回就表示该MediaPlayer对象已经进入了Prepared状态;
  • 调用prepareAsync()方法,此方法会使此MediaPlayer对象进入Preparing状态并返回,而内部的播放引擎会继续未完成的准备工作。当同步版本返回时或异步版本的准备工作完全完成时就会调用客户端程序员提供的OnPreparedListener.onPrepared()监听方法。可以调用MediaPlayer.setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)方法来注册OnPreparedListener.
  • MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等
  1. 要开始播放,必须调用start()方法。当此方法成功返回时,MediaPlayer的对象处于Started状态。 isPlaying()方法可以被调用来测试某个MediaPlayer对象是否在Started状态。
  • 当处于Started状态时,内部播放引擎会调用客户端程序员提供的OnBufferingUpdateListener.onBufferingUpdate()回调方法,此回调方法允许应用程序追踪流播放的缓冲的状态。
  1. 播放可以被暂停,停止,以及调整当前播放位置。当调用pause()方法并返回时,会使MediaPlayer对象进入Paused状态。
  • 注意Started与Paused状态的相互转换在内部的播放引擎中是异步的。所以可能需要一点时间在isPlaying()方法中更新状态,若在播放流内容,这段时间可能会有几秒钟。因此用自己定义的isPlaying做状态值较好

  • 调用start()方法会让一个处于Paused状态的MediaPlayer对象从之前暂停的地方恢复播放。

  • 已经处于Paused状态的MediaPlayer对象pause()方法没有影响。

  • 调用stop()方法会停止播放,并且还会让一个处于Started,Paused,Prepared或PlaybackCompleted状态的MediaPlayer进入Stopped状态。

  1. 当播放到流的末尾,播放就完成了
    若没有开启循环模式,那么内部的播放引擎会调用客户端程序员提供的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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容