美文网首页Android开发录androidAndroid开发
用MediaPlayer+TextureView封装一个完美实现

用MediaPlayer+TextureView封装一个完美实现

作者: xiaoyanger | 来源:发表于2017-05-22 15:58 被阅读24374次

项目已添加IjkPlayer支持,后续逐渐完善其他功能。
地址:https://github.com/xiaoyanger0825/NiceVieoPlayer

为什么使用TextureView

在Android总播放视频可以直接使用VideoViewVideoView是通过继承自SurfaceView来实现的。SurfaceView的大概原理就是在现有View的位置上创建一个新的Window,内容的显示和渲染都在新的Window中。这使得SurfaceView的绘制和刷新可以在单独的线程中进行,从而大大提高效率。但是呢,由于SurfaceView的内容没有显示在View中而是显示在新建的Window中, 使得SurfaceView的显示不受View的属性控制,不能进行平移,缩放等变换,也不能放在其它RecyclerViewScrollView中,一些View中的特性也无法使用。

TextureView是在4.0(API level 14)引入的,与SurfaceView相比,它不会创建新的窗口来显示内容。它是将内容流直接投放到View中,并且可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中使用。

TextureView被创建后不能直接使用,必须要在它被它添加到ViewGroup后,待SurfaceTexture准备就绪才能起作用(看TextureView的源码,TextureView是在绘制的时候创建的内部SurfaceTexture)。通常需要给TextureView设置监听器SurfaceTextuListener

mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture准备就绪
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture缓冲大小变化
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // SurfaceTexture即将被销毁
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // SurfaceTexture通过updateImage更新
    }
});

SurfaceTexture的准备就绪、大小变化、销毁、更新等状态变化时都会回调相对应的方法。当TextureView内部创建好SurfaceTexture后,在监听器的onSurfaceTextureAvailable方法中,用SurfaceTexture来关联MediaPlayer,作为播放视频的图像数据来源。

SurfaceTexture作为数据通道,把从数据源(MediaPlayer)中获取到的图像帧数据转为GL外部纹理,交给TextureVeiw作为View heirachy中的一个硬件加速层来显示,从而实现视频播放功能。

MediaPlayer介绍

MediaPlayer是Android原生的多媒体播放器,可以用它来实现本地或者在线音视频的播放,同时它支持https和rtsp

MediaPlayer定义了各种状态,可以理解为是它的生命周期。

MediaPlayer状态图(生命周期)

这个状态图描述了MediaPlayer的各种状态,以及主要方法调用后的状态变化。

MediaPlayer的相关方法及监听接口:

方法 介绍 状态
setDataSource 设置数据源 Initialized
prepare 准备播放,同步 Preparing —> Prepared
prepareAsync 准备播放,异步 Preparing —> Prepared
start 开始或恢复播放 Started
pause 暂停 Paused
stop 停止 Stopped
seekTo 到指定时间点位置 PrePared/Started
reset 重置播放器 Idle
setAudioStreamType 设置音频流类型 --
setDisplay 设置播放视频的Surface --
setVolume 设置声音 --
getBufferPercentage 获取缓冲半分比 --
getCurrentPosition 获取当前播放位置 --
getDuration 获取播放文件总时间 --
内部回调接口 介绍 状态
OnPreparedListener 准备监听 Preparing ——>Prepared
OnVideoSizeChangedListener 视频尺寸变化监听 --
OnInfoListener 指示信息和警告信息监听 --
OnCompletionListener 播放完成监听 PlaybackCompleted
OnErrorListener 播放错误监听 Error
OnBufferingUpdateListener 缓冲更新监听 --

MediaPlayer在直接new出来之后就进入了Idle状态,此时可以调用多个重载的setDataSource()方法从idle状态进入Initialized状态(如果调用setDataSource()方法的时候,MediaPlayer对象不是出于Idle状态,会抛异常,可以调用reset()方法回到Idle状态)。

调用prepared()方法和preparedAsync()方法进入Prepared状态,prepared()方法直接进入Parpared状态,preparedAsync()方法会先进入PreParing状态,播放引擎准备完毕后会通过OnPreparedListener.onPrepared()回调方法通知Prepared状态。

在Prepared状态下就可以调用start()方法进行播放了,此时进入started()状态,如果播放的是网络资源,Started状态下也会自动调用客户端注册的OnBufferingUpdateListener.OnBufferingUpdate()回调方法,对流播放缓冲的状态进行追踪。

pause()方法和start()方法是对应的,调用pause()方法会进入Paused状态,调用start()方法重新进入Started状态,继续播放。

stop()方法会使MdiaPlayer从Started、Paused、Prepared、PlaybackCompleted等状态进入到Stoped状态,播放停止。

当资源播放完毕时,如果调用了setLooping(boolean)方法,会自动进入Started状态重新播放,如果没有调用则会自动调用客户端播放器注册的OnCompletionListener.OnCompletion()方法,此时MediaPlayer进入PlaybackCompleted状态,在此状态里可以调用start()方法重新进入Started状态。

封装考虑

MediaPlayer的方法和接口比较多,不同的状态调用各个方法后状态变化情况也比较复杂。播放相关的逻辑只与MediaPlayer的播放状态和调用方法相关,而界面展示和UI操作很多时候都需要根据自己项目来定制。参考原生的VideoView,为了解耦和方便定制,把MediaPlayer的播放逻辑和UI界面展示及操作相关的逻辑分离。我是把MediaPlayer直接封装到NiceVideoPlayer中,各种UI状态和操作反馈都封装到NiceVideoPlayerController里面。如果需要根据不同的项目需求来修改播放器的功能,就只重写NiceVideoPlayerController就可以了。

NiceVideoPlayer

首先,需要一个FrameLayout容器mContainer,里面有两层内容,第一层就是展示播放视频内容的TextureView,第二层就是播放器控制器mController。那么自定义一个NiceVideoPlayer继承自FrameLayout,将mContainer添加到当前控件:

public class NiceVideoPlayer extends FrameLayout{

    private Context mContext;
    private NiceVideoController mController;
    private FrameLayout mContainer;
    
    public NiceVideoPlayer(Context context) {
        this(context, null);
    }

    public NiceVideoPlayer(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    private void init() {
         mContainer = new FrameLayout(mContext);
         mContainer.setBackgroundColor(Color.BLACK);
         LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);
    }
}

添加setUp方法来配置播放的视频资源路径(本地/网络资源):

public void setUp(String url, Map<String, String> headers) {
        mUrl = url;
        mHeaders = headers;
    }

用户要在mController中操作才能播放,因此需要在播放之前设置好mController:

public void setController(NiceVideoPlayerController controller) {
    mController = controller;
    mController.setNiceVideoPlayer(this);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mController, params);
}

用户在自定义好自己的控制器后通过setController这个方法设置给播放器进行关联。

触发播放时,NiceVideoPlayer将展示视频图像内容的mTextureView添加到mContainer中(在mController的下层),同时初始化mMediaPlayer,待mTextureView的数据通道SurfaceTexture准备就绪后就可以打开播放器:

public void start() {
    initMediaPlayer();  // 初始化播放器
    initTextureView();  // 初始化展示视频内容的TextureView
    addTextureView();   // 将TextureView添加到容器中
}

private void initTextureView() {
    if (mTextureView == null) {
        mTextureView = new TextureView(mContext);
        mTextureView.setSurfaceTextureListener(this);
    }
}

private void addTextureView() {
    mContainer.removeView(mTextureView);
    LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mTextureView, 0,  params);
}

private void initMediaPlayer() {
    if (mMediaPlayer == null) {
        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);

        mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
        mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
        mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
        mMediaPlayer.setOnErrorListener(mOnErrorListener);
        mMediaPlayer.setOnInfoListener(mOnInfoListener);
        mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
    }
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    // surfaceTexture数据通道准备就绪,打开播放器
    openMediaPlayer(surface);
}

private void openMediaPlayer(SurfaceTexture surface) {
    try {
        mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUrl), mHeaders);
        mMediaPlayer.setSurface(new Surface(surface));
        mMediaPlayer.prepareAsync();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

打开播放器调用prepareAsync()方法后,mMediaPlayer进入准备状态,准备就绪后就可以开始:

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
    }
};

NiceVideoPlayer的这些逻辑已经实现视频播放了,操作相关以及UI展示的逻辑需要在控制器NiceVideoPlayerController中来实现。但是呢,UI的展示和反馈都需要依据播放器当前的播放状态,所以需要给播放器定义一些常量来表示它的播放状态:

public static final int STATE_ERROR = -1;          // 播放错误
public static final int STATE_IDLE = 0;            // 播放未开始
public static final int STATE_PREPARING = 1;       // 播放准备中
public static final int STATE_PREPARED = 2;        // 播放准备就绪
public static final int STATE_PLAYING = 3;         // 正在播放
public static final int STATE_PAUSED = 4;          // 暂停播放
// 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
public static final int STATE_BUFFERING_PLAYING = 5;
// 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停)
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7;       // 播放完成

播放视频时,mMediaPlayer准备就绪(Prepared)后没有马上进入播放状态,中间有一个时间延迟时间段,然后开始渲染图像。所以将Prepared——>“开始渲染”中间这个时间段定义为STATE_PREPARED

如果是播放网络视频,在播放过程中,缓冲区数据不足时mMediaPlayer内部会停留在某一帧画面以进行缓冲。正在缓冲时,mMediaPlayer可能是在正在播放也可能是暂停状态,因为在缓冲时如果用户主动点击了暂停,就是处于STATE_BUFFERING_PAUSED,所以缓冲有STATE_BUFFERING_PLAYINGSTATE_BUFFERING_PAUSED两种状态,缓冲结束后,恢复播放或暂停。

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
        mCurrentState = STATE_PREPARED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onPrepared ——> STATE_PREPARED");
    }
};

private MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener
        = new MediaPlayer.OnVideoSizeChangedListener() {
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        LogUtil.d("onVideoSizeChanged ——> width:" + width + ",height:" + height);
    }
};

private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
    }
};

private MediaPlayer.OnErrorListener mOnErrorListener
        = new MediaPlayer.OnErrorListener() {
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mCurrentState = STATE_ERROR;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onError ——> STATE_ERROR ———— what:" + what);
        return false;
    }
};

private MediaPlayer.OnInfoListener mOnInfoListener
        = new MediaPlayer.OnInfoListener() {
    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
            // 播放器渲染第一帧
            mCurrentState = STATE_PLAYING;
            mController.setControllerState(mPlayerState, mCurrentState);
            LogUtil.d("onInfo ——> MEDIA_INFO_VIDEO_RENDERING_START:STATE_PLAYING");
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
            // MediaPlayer暂时不播放,以缓冲更多的数据
            if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_BUFFERING_PAUSED;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PAUSED");
            } else {
                mCurrentState = STATE_BUFFERING_PLAYING;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PLAYING");
            }
            mController.setControllerState(mPlayerState, mCurrentState);
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
            // 填充缓冲区后,MediaPlayer恢复播放/暂停
            if (mCurrentState == STATE_BUFFERING_PLAYING) {
                mCurrentState = STATE_PLAYING;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PLAYING");
            }
            if (mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_PAUSED;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PAUSED");
            }
        } else {
            LogUtil.d("onInfo ——> what:" + what);
        }
        return true;
    }
};

private MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener
        = new MediaPlayer.OnBufferingUpdateListener() {
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        mBufferPercentage = percent;
    }
};

mController.setControllerState(mPlayerState, mCurrentState)mCurrentState表示当前播放状态,mPlayerState表示播放器的全屏、小窗口,正常三种状态。

public static final int PLAYER_NORMAL = 10;        // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11;   // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12;   // 小窗口播放器

定义好播放状态后,开始暂停等操作逻辑也需要根据播放状态调整:

@Override
public void start() {
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

@Override
public void restart() {
    if (mCurrentState == STATE_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PLAYING");
    }
    if (mCurrentState == STATE_BUFFERING_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_BUFFERING_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PLAYING");
    }
}

@Override
public void pause() {
    if (mCurrentState == STATE_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PAUSED");
    }
    if (mCurrentState == STATE_BUFFERING_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_BUFFERING_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PAUSED");
    }
}

reStart()方法是暂停时继续播放调用。

全屏、小窗口播放的实现

可能最能想到实现全屏的方式就是把当前播放器的宽高给放大到屏幕大小,同时隐藏除播放器以外的其他所有UI,并设置成横屏模式。但是这种方式有很多问题,比如在列表(ListView或RecyclerView)中,除了放大隐藏外,还需要去计算滑动多少距离才刚好让播放器与屏幕边缘重合,退出全屏的时候还需要滑动到之前的位置,这样实现逻辑不但繁琐,而且和外部UI偶合严重,后面改动维护起来非常困难(我曾经就用这种方式被坑了无数道)。

分析能不能有其他更好的实现方式呢?

整个播放器由mMediaPalyer+mTexutureView+mController组成,要实现全屏或小窗口播放,我们只需要挪动播放器的展示界面mTexutureView和控制界面mController即可。并且呢我们在上面定义播放器时,已经把mTexutureViewmController一起添加到mContainer中了,所以只需要将mContainer从当前视图中移除,并添加到全屏和小窗口的目标视图中即可。

那么怎么确定全屏和小窗口的目标视图呢?

我们知道每个Activity里面都有一个android.R.content,它是一个FrameLayout,里面包含了我们setContentView的所有控件。既然它是一个FrameLayout,我们就可以将它作为全屏和小窗口的目标视图。

我们把从当前视图移除的mContainer重新添加到android.R.content中,并且设置成横屏。这个时候还需要注意android.R.content是不包括ActionBar和状态栏的,所以要将Activity设置成全屏模式,同时隐藏ActionBar

@Override
public void enterFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) return;

    // 隐藏ActionBar、状态栏,并横屏
    NiceUtil.hideActionBar(mContext);
    NiceUtil.scanForActivity(mContext)
            .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

    this.removeView(mContainer);
    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_FULL_SCREEN;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_FULL_SCREEN");
}

退出全屏也就很简单了,将mContainerandroid.R.content中移除,重新添加到当前视图,并恢复ActionBar、清除全屏模式就行了。

public boolean exitFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) {
        NiceUtil.showActionBar(mContext);
        NiceUtil.scanForActivity(mContext)
                .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}

切换横竖屏时为了避免Activity重新走生命周期,别忘了需要在Manifest.xmlactivity标签下添加如下配置:

android:configChanges="orientation|keyboardHidden|screenSize"

进入小窗口播放和退出小窗口的实现原理就和全屏功能一样了,只需要修改它的宽高参数:

@Override
public void enterTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) return;

    this.removeView(mContainer);

    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    // 小窗口的宽度为屏幕宽度的60%,长宽比默认为16:9,右边距、下边距为8dp。
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f),
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f));
    params.gravity = Gravity.BOTTOM | Gravity.END;
    params.rightMargin = NiceUtil.dp2px(mContext, 8f);
    params.bottomMargin = NiceUtil.dp2px(mContext, 8f);

    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_TINY_WINDOW;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_TINY_WINDOW");
}

@Override
public boolean exitTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) {
        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}

这里有个特别需要注意的一点:

mContainer移除重新添加后,mContainer及其内部的mTextureViewmController都会重绘,mTextureView重绘后,会重新new一个SurfaceTexture,并重新回调onSurfaceTextureAvailable方法,这样mTextureView的数据通道SurfaceTexture发生了变化,但是mMediaPlayer还是持有原先的mSurfaceTexut,所以在切换全屏之前要保存之前的mSufaceTexture,当切换到全屏后重新调用onSurfaceTextureAvailable时,将之前的mSufaceTexture重新设置给mTexutureView。这样就保证了切换时视频播放的无缝衔接。

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    if (mSurfaceTexture == null) {
        mSurfaceTexture = surfaceTexture;
        openMediaPlayer();
    } else {
        mTextureView.setSurfaceTexture(mSurfaceTexture);
    }
}

NiceVideoPlayerControl

为了解除NiceVideoPlayerNiceVideoPlayerController的耦合,把NiceVideoPlayer的一些功能性和判断性方法抽象到NiceVideoPlayerControl接口中。

public interface NiceVideoPlayerControl {

    void start();
    void restart();
    void pause();
    void seekTo(int pos);

    boolean isIdle();
    boolean isPreparing();
    boolean isPrepared();
    boolean isBufferingPlaying();
    boolean isBufferingPaused();
    boolean isPlaying();
    boolean isPaused();
    boolean isError();
    boolean isCompleted();

    boolean isFullScreen();
    boolean isTinyWindow();
    boolean isNormal();

    int getDuration();
    int getCurrentPosition();
    int getBufferPercentage();

    void enterFullScreen();
    boolean exitFullScreen();
    void enterTinyWindow();
    boolean exitTinyWindow();

    void release();
}

NiceVideoPlayer实现这个接口即可。

NiceVideoPlayerManager

同一界面上有多个视频,或者视频放在ReclerView或者ListView的容器中,要保证同一时刻只有一个视频在播放,其他的都是初始状态,所以需要一个NiceVideoPlayerManager来管理播放器,主要功能是保存当前已经开始了的播放器。

public class NiceVideoPlayerManager {

    private NiceVideoPlayer mVideoPlayer;

    private NiceVideoPlayerManager() {
    }

    private static NiceVideoPlayerManager sInstance;

    public static synchronized NiceVideoPlayerManager instance() {
        if (sInstance == null) {
            sInstance = new NiceVideoPlayerManager();
        }
        return sInstance;
    }

    public void setCurrentNiceVideoPlayer(NiceVideoPlayer videoPlayer) {
        mVideoPlayer = videoPlayer;
    }

    public void releaseNiceVideoPlayer() {
        if (mVideoPlayer != null) {
            mVideoPlayer.release();
            mVideoPlayer = null;
        }
    }

    public boolean onBackPressd() {
        if (mVideoPlayer != null) {
            if (mVideoPlayer.isFullScreen()) {
                return mVideoPlayer.exitFullScreen();
            } else if (mVideoPlayer.isTinyWindow()) {
                return mVideoPlayer.exitTinyWindow();
            } else {
                mVideoPlayer.release();
                return false;
            }
        }
        return false;
    }
}

采用单例,同时,onBackPressedActivity中用户按返回键时调用。
NiceVideoPlayerstart方法以及onCompleted需要修改一下,保证开始播放一个视频时要先释放掉之前的播放器;同时自己播放完毕,要将NiceVideoPlayerManager中的mNiceVideoPlayer实例置空,避免内存泄露。

// NiceVideoPlayer的start()方法。
@Override
public void start() {
    NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
    NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(this);
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

// NiceVideoPlayer中的onCompleted监听。
private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
        NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(null);
    }
};

NiceVideoPlayerController

播放控制界面上,播放、暂停、播放进度、缓冲动画、全屏/小屏等触发都是直接调用播放器对应的操作的。需要注意的就是调用之前要判断当前的播放状态,因为有些状态下调用播放器的操作可能引起错误(比如播放器还没准备就绪,就去获取当前的播放位置)。

播放器在触发相应功能的时候都会调用NiceVideoPlayerControllersetControllerState(int playerState, int playState)这个方法来让用户修改UI。

不同项目都可能定制不同的控制器(播放操作界面),这里我就不详细分析实现逻辑了,大致功能就类似腾讯视频的热点列表中的播放器。其中横向滑动改变播放进度、左侧上下滑动改变亮度,右侧上下滑动改变亮度等功能在代码中都有实现。代码有点长,就不贴了,需要的直接下载源码

使用

mNiceVideoPlayer.setUp(url, null);
NiceVideoPlayerController controller = new NiceVideoPlayerController(this);
controller.setTitle(title);
controller.setImage(imageUrl);
mNiceVideoPlayer.setController(controller);

RecyclerView或者ListView中使用时,需要监听itemViewdetached

mRecyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
    @Override
    public void onChildViewAttachedToWindow(View view) {

    }

    @Override
    public void onChildViewDetachedFromWindow(View view) {
        NiceVideoPlayer niceVideoPlayer = (NiceVideoPlayer) view.findViewById(R.id.nice_video_player);
        if (niceVideoPlayer != null) {
            niceVideoPlayer.release();
        }
    }
});

ItemViewdetach窗口时,需要释放掉itemView内部的播放器。

效果图

最后

整个功能有参考节操播放器,但是自己这样封装和节操播放器还是有很大差异:一是分离了播放功能和控制界面,定制只需修改控制器即可。二是全屏/小窗口没有新建一个播放器,只是挪动了播放界面和控制器,不用每个视频都需要新建两个播放器,也不用同步状态。


MediaPlayer有很多格式不支持,项目已添加IjkPlayer的扩展支持,可以切换IjkPlayer和原生MediaPlayer,后续还会考虑添加ExoPlayer,同时也会扩展更多功能。

如果有错误和更好的建议都请提出,源码已上传GitHub,欢迎Star,谢谢!。

源码:https://github.com/xiaoyanger0825/NiceVieoPlayer


参考:
Android TextureView简易教程
视频画面帧的展示控件SurfaceView及TextureView对比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期详解
节操播放器 https://github.com/lipangit/JieCaoVideoPlayer

相关文章

  • 用MediaPlayer+TextureView封装一个完美实现

    项目已添加IjkPlayer支持,后续逐渐完善其他功能。地址:https://github.com/xiaoyan...

  • 【转】“回到顶部”代码简单封装

    原文链接:完美实现一个“回到顶部” 简单封装 调用 效果查看戳scrollIntoView.html

  • 面向对象-组件

    代码地址 封装一个轮播组件 我的实现 封装一个曝光加载组件 我的实现 封装一个 Tab 组件 我的实现 封装一个 ...

  • iOS -- 图片点击放大BCImageBrowser的使用

    图片点击放大,再次点击返回原视图.完美封装,一个类一句代码即可调用.IOS完美实现 导入头文件 引入此类之后,为自...

  • Java做爬虫也很牛

    首先我们封装一个Http请求的工具类,用HttpURLConnection实现,当然你也可以用HttpClient...

  • 面向对象组件封装

    1.封装一个轮播组件 实现代码 2.封装一个曝光加载组件 实现代码 3.封装一个Tab组件 实现代码 4.封装一个...

  • 完美运动框架

    完美运动框架封装 完美运动框架调用 封装完美运动框架遇到的问题? 1.timer问题如果我们只保存一个timer变...

  • 实现验证码图片几种方式思考

    实现图片验证码 1.纯js方式2.后端已封装好的库方式3.自已封装的方式 PIL四种实现方式,越来越趋于完美 方式...

  • jquery之弹框

    用jquery实现弹框脚本的封装, 使用示例 页面显示

  • Swift之SQLite简单封装

    昨天回顾了OC语言下使用并封装SQLite,今天趁热打铁再试着用Swift实现一下对​SQLite的封装,实现基本...

网友评论

  • 赐我一死:不能在ListView用
  • 简书20210618:分享按钮点击的回调监听有没有?
  • f56d1558904a:我的项目添加你的依赖,点击播放找不到so文件java.lang.UnsatisfiedLinkError: dalvik.sys.... couldn't find "libijkffmpeg.so",大神请问如何添加这个so包。
  • a2b52b16b576:大神 我想实现预览框圆角怎么写哇
  • 飞的鱼33:你好,项目写得很好,我有个想法,就是播放一个视频集合可以吗?就是分段视频播放那样
  • 一杯酒几分愁:项目挺好的,不过要是变成小窗口后能够拖拉就更好了
  • gyymz1993:我到时候把在你的demo上加的一些网络处理的弄个demo发出来
  • gyymz1993:出现过一个问题 我们是暂停是拖动进度后继续暂停状态,在所有的vivo手机上都会出现黑屏 好像是当前的那一帧转Bitmap失败
  • mrFessible:谢谢,写的很详细,懂了很多
  • 1dot4:bug,注释掉onViewRecycled里面的代码,recycleView会出现两个一样的视频在播放(例如我播放第一个,滑到第7个,第七个也在播放)
  • 根艮哏艮根:显示全屏的时候不播放,点击暂停按钮也不起作用,楼主,这个怎么解决呢?
  • f9320167088f:iphone10录制的视频,播放的时候只显示声音不显示画面,我把视频地址放到你的demo中发现的
  • Gear_033e:textureview播放视频是引起UI刷新问题怎么解决?比如我有一个拖拽控件,播放视频的时候就拖动不了。
  • Liekkas_BJ:混淆打包后会报java.lang.unsatisfiedlinkerror
  • Asny_ac2d:您好,我在第一次播放的过程中,缓冲进度条没反应,但点重新播放的话,它又有了,这个是什么问题的。
  • 不正就是歪573:你好,为什么Sample里面的进度条,往前拖动进度条后,还会往后退几秒。特别是视频大小1分钟以内的,越小复现概率越高
    gyymz1993:@不正就是歪arthas 在进度回调里处理一下
  • 陈文超happylion:这大体上就是节操播放器的实现啊
    xiaoyanger:@陈文超happylion 有参考节操,但和节操还是有很大差异的哦
  • 吐必南波丸:楼主看见能恢复下吗
  • 吐必南波丸:楼主方便加个微信好友吗?求助解决点问题
  • 吐必南波丸:列表播放好用的url还是会播放错误
  • 94af91c5ca0a:您好作者,我觉得你处理Home按键的逻辑太过复杂,并且存在bug,比如如果用户按下的任务键切换到其他应用再切换回来的处理并没有做,包括Activity切换到其他界面进入后台的处理也没有做,我觉得只需要在这几个生命周期回调的方法里面加入相应的处理就好了,不需要监听什么Home键:
    @Override
    protected void onPause() {
    super.onPause();
    NiceVideoPlayerManager.instance().suspendNiceVideoPlayer();
    }

    @Override
    protected void onResume() {
    super.onResume();
    NiceVideoPlayerManager.instance().resumeNiceVideoPlayer();
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
    }

    @Override
    public void onBackPressed() {
    if (NiceVideoPlayerManager.instance().onBackPressd()) {
    return;
    }
    super.onBackPressed();
    }
  • 94af91c5ca0a:作者你好,有一个bug,播放完成之后点击重新播放没有从头开始播放,而是从上一次播放的位置开始播放的,我是在重新播放的事件里面加了一句continueFromLastPosition = false;不知道这样写会影响其他地方吗?
  • 风岫主人:您好,你这个是不是对视频链接的类型有要求?我用通用的视频播放地址,无法播放。能告诉我怎么解决吗
  • 小宝_8428:你好,我有两个地方用到视频播放,为啥一个能播放,另一个点击播放按钮没反应呢?数据是正确的
  • WeberLisper:这个是写得真好了
  • c456aada96ce:大神,您好,请问下,在视频播放结束以后会出现重新播放和分享,重新播放是可以的,分享功能是不行的.如果我不是很需要分享这个功能,有没有什么方法可以隐藏掉这个分享功能呢? 就是让播放结束后分享这个功能可选择化
    e7b3272c5c3f:我也遇到这样的问题,请问你们是怎么解决的
  • 66e338f31e5d:大神设置视频封面怎么设置?controller.setImage(); 这个传int类型的id值?
    94af91c5ca0a:Glide.with(this)
    .load("http://f.hiphotos.baidu.com/image/pic/item/b7003af33a87e95050b6297f1a385343fbf2b404.jpg";)
    .placeholder(R.mipmap.ic_launcher)
    .crossFade()
    .into(controller.imageView());

    有没有用心看github上面的文档说明?给你轮子都不会用
  • 96df65742090:大神你好,我发现一个播放直播的bug,播放直播流只能播放2-3秒就暂停卡住了,比如这个链接http://phoneliveal.mgtv.com/nn_live/nn_x64/aWQ9SE5EU0pNUFAzNjAmY2RuZXhfaWQ9YWxfcGhvbmVfbGl2ZTMmbmZ0PXRz/HNDSJMPP360.m3u8
  • 向日葵lll:allprojects {
    repositories {
    ...
    maven { url 'https://jitpack.io' }
    }
    }

    dependencies {
    compile 'com.github.xiaoyanger0825:NiceVieoPlayer:v2.2'
    }
    我按照这个配置。 结果报错。。提示系统找不到指定文件
  • 向日葵lll:我早recyclerview 视频列表中进行更改,将mp4url改成 mp3url,结果播放的时候 无法显示播放进度。
  • 向日葵lll:播放mp3的时候,怎么能像mp4一样 显示播放进度,暂停/播放呢
  • 北京朝阳区精神病院院长:你好大神,我用你的开源项目NiceVideo,请问下我播放在 sd卡上的视频,有声音但是没有视频图像,怎么解决,是因为6.0运行权限的问题吗
  • c9ccff0b60ea:您好,今天使用了这个,但是默认图,就是美女的就是一直显示,怎么去掉,我已经用Glide加载了默认图片,可是那个美女图片还是有,请问怎么解决
    北京朝阳区精神病院院长: .placeholder(R.drawable.img_default)换成你想要的图片资源
  • babfb6451761:楼主威武,这几天折腾着搞一个recyclerview播放视频的demo,本来想着通过隐藏各种控件实现全屏效果的,代码一敲出来,超级臃肿,后期维护要搞死人了,果断放弃。跑去看了看textureview 和 mediaplayer的源码,本来想自己封装一个videoview的,懒癌一发作,用楼主的好了:stuck_out_tongue:
  • 神龟威少:Android 7.0播放失败。。。运行时权限怎么处理的呢?
  • 21cb83a671c1:大神 我想在列表里面获取当前播放的视频的时间 有没有方法之类的。
  • Answer_yzpppp:楼主大神。有个问题想请教一下。。由于项目对APP大小有限制,没有使用IJKPlayer,使用的是原生的MediaPlaayer,但是现在调用prepareAsync后等待的时间比较长才能开始播放。想请问一下这大概是什么原因引起的?有什么解决方法吗?
    Answer_yzpppp:@xiaoyanger 好吧。谢谢啦
    xiaoyanger:@Answer_yzpppp 原生的MediaPlayer本来在准备的时候就很慢,这个目前还没有什么好的解决方式呢
  • 641cc18205d7:厉害了大神,感觉的您的思路给我打开一扇通往小神级别的大门啊,还有不知道您最新的代码有上传吗,就是IJKpleyer的?:stuck_out_tongue:
    xiaoyanger:@杨玉安 里面还是有MediaPlayer的,可以切换
    641cc18205d7:那我还能找到只有mediaplayer的代码吗?
    xiaoyanger: @杨玉安 最新的代码在github
  • i冰点:锁屏时没有调用onSurfaceTextureDestroyed,按home键时调用了onSurfaceTextureDestroyed...
    onSurfaceTextureDestroyed是在什么情况下调用的???
    谢谢
  • MrTrying:有个问题,我在我app中集成,直接不能播放
    xiaoyanger:@MrTrying 你参考下demo呢,看看demo能不能播放呢
    MrTrying:@xiaoyanger 调用start方法没反应,点击播放按钮也不能播放
    不知道是不是我集成的时候有什么不对
    xiaoyanger:@MrTrying 怎么不能直接播放呢
  • 才兄说:作者建议你可以播放器在小窗时,可以做成哪种类似悬浮窗口的形式播放,市面上很多这样的效果,并且可以拖拽,直接改变TextureView的位置属性可以做到;或者使用悬浮窗功能实现,都可以尝试下
    xiaoyanger:@天鬼 可以,谢谢。
    才兄说:@xiaoyanger 好的,有需要这边可以提供些demo给你,方便你参考,把东西做好也让大家受益了~~~
    xiaoyanger: @天鬼 后续会尝试加上去
  • 才兄说:谢谢分享,开源的东西很棒,一般很少给,你这个给了star
    xiaoyanger: @天鬼 谢谢
  • 暇思茕然:您好 请问视频播放结束后怎么隐藏视频上的分享按钮?
    xiaoyanger: @暇思茕然 你如果不需要分享按钮的话,可以自己定义controller,去掉布局里面的分享按钮就行。
  • 23422c267505:你好,我想问下,播放监听是哪个方法,就是暂停和播放完成后,我需要在自己的工程中进行一些操作
  • 86654a2d3e1e:vivo Xplay 5 android5.1.1正常播放异常:
    86654a2d3e1e:@汪洋大海中的小木头 这个问题没有解决,我@了作者,他说要自己编译底层库,没得办法 ,我还是用mediaPlayer自定义了一个textureView的播放器
    e91773c9274e:@起名字怎么这么难 这个问题
    我也遇到了
    你怎么解决的啊
    86654a2d3e1e: Process: com.spreadit.spreaditapp, PID: 24006
    java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.spreadit.spreaditapp-1/base.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_dependencies_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_0_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_1_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_2_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_3_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_4_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_5_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_7_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.spreadit.spreaditapp-1/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/data/app/com.spreadit.spreaditapp-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn't find "libijkffmpeg.so"
    at java.lang.Runtime.loadLibrary(Runtime.java:370)
  • 泽毛:正准备学习视频的相关知识,博主的文章和例子都很好理解,点赞!😍
    xiaoyanger: @泽毛 谢谢。😁
  • Y了个F:fragment中全屏播放 按下回退键怎么回到原来状态
  • deb1199d2c5d:大神,发现一个问题,使用原生MediaPlayer播放时候,播放结束后重新播放会一直准备中
    deb1199d2c5d:@xiaoyanger 安卓6.0
    deb1199d2c5d:@xiaoyanger 魅蓝5s
    xiaoyanger: @某卓程序猿 是什么机型和系统呢
  • 笔墨Android:楼主问你一个事情,为什么有的视频播放的时候只有声音没有视频,显示的是默认的黑色
  • a375ed21fb82:终于发现一个不错,还能用的了,请问楼主,这个能支持播放项目内的视频吗,比如raw下
    xiaoyanger:支持的哈
    Android_小生:当然支持,本地/网络都可以,把路径/网址传到 setUp() 方法就行。
  • Android_小生:非常感谢楼主的分享,对自己当前的业务很有帮助。有个问题,在播放本地视频的时候,手动拖拽 seekbar,我手机出现如果拖拽到的位置是小于视频总长度的一半,视频会重新开始播放,而拖拽到大于视频总长度一半的位置时,视频会回到总长度一半的位置播放,视频总是不能在拖拽的确定位置播放。网络视频播放没有这个问题。
    xiaoyanger: @世界很Ku 我就说我测试也没发现呢,可能是视频文件有损坏
    Android_小生:是 TMD 的我视频文件的问题,换了一个视频后很正常,我之前还 debug 的很起劲...
  • 海荣荣:大神,我现在想在framelayout布局上加载弹幕控件,可是我addview后,并没有看到我的弹幕,是被隐藏在textureview下面了吗
    海荣荣: @xiaoyanger 我想再问下,为什么咱们的demo跑在其他手机上会报依赖库没依赖上呢?有些手机不会报,有些手机报
    xiaoyanger:不会隐藏在textureview下面的哈,直接添加到mContainer中,不给定索引值,它会自动添加到最上层的
  • 海荣荣:framelayout动态显示控件位置根据addview中的第二个参数就可以更改吗?
    xiaoyanger:addView的第二个参数是添加的子View的索引位置,添加进去了没法改这个位置了,除非有其他子View添加到它前面。
  • 冬虫不可以语夏:大神,您这个seekto方法是怎么调用的,我穿了毫秒数进去怎么没反应呢?
    冬虫不可以语夏: @xiaoyanger 大神,我这边需要在视频结束播放时,比如说手机的back键或home键,记录一下当前播放时间,我在onstop 里去调用,得到的是00:00,求解? @xiaoyanger
    xiaoyanger:@不知寒号 调用mNiceVideoPlayer的seekTo传毫秒进去应该没问题呢,你看下计算的position有问题没呢。
    冬虫不可以语夏: @xiaoyanger
  • 月灯:你好,有一个问题就是,播放宽小于高的视频的时候,也就是手机竖着录制的视频时,会把视频强制横向播放,请问大概在哪里能调试
    月灯:@xiaoyanger 唔 我这里大概要的是,如果高大于宽的情况下,是视频居中,两侧黑色填充...现在在那个监听里面,获取的宽高。直接是横向播放的宽高,而不是视频本身的宽高。
    xiaoyanger:可以在OnVideoSizeChangedListener中堆宽高按照你需要的比例进行调整
  • 冬虫不可以语夏:大神,支持本地视频播放吗
    冬虫不可以语夏: @xiaoyanger 想加上视频下载,有什么建议吗?大神
    xiaoyanger: @不知寒号 可以的,不过6.0的运行时权限没添加,需要自己提前检查运行时权限
  • b3ceb683ddfb:楼主,怎么多视频循环播放啊,求指教!
  • Especially:楼主,如何修改 才能支持.m3u8格式呀 项目视频有MP4也有m3u8的
    Especially:@xiaoyanger IjkPlayer 的不行 我是切换为原生的MediaPlayer 可以了
    Especially:@xiaoyanger :blush: 灰常感谢 回复的很及时,我刚也发现了 谢谢
    xiaoyanger:@Especially丶 项目已经添加了IjkPlayer,可以将MediaPlayer切换为IjkPlayer,ijk是可以支持m3u8的。
  • DaydreamC:楼主,最近在看你这个项目,想加一下滑动控制 音量、亮度、快进后退。 但不知道加在哪个类,请指教一下。
    xiaoyanger:@不知寒号 最新版的集成了ijkplayer,基本的格式都支持,要实现边播放边缓存的话,可以参考https://github.com/CarGuo/GSYVideoPlayer
    冬虫不可以语夏:大神,您这个播放器支持几种播放格式呢?我想实现缓存功能,有什么建议吗?谢谢
    xiaoyanger:加载这个类NiceVideoPlayerController里面,通过拦截touch事件来处理就行了,后续我会加上的,你现在急需的话可以参考一下节操播放器的滑动控制 音量、亮度、快进后退逻辑。
  • boboyuwu:SurfaceView的显示不受View的属性控制,不能进行平移,缩放等变换,也不能放在其它RecyclerView或ScrollView中,一些View中的特性也无法使用

    VedioView集成SurfaceView为什么VedioView可以加到RecyclerView和ScrollView中啊
    xiaoyanger: @lance_0ea7 播放本地视频需要运行时权限,源码中并未加入,6.0以上只有在播放前提前判断运行时权限并允许,后续我会看看能不能直接在源码中加入。
    766a546ee35d:您好,你的源码下载下来,播放手机本地视频,一直提示没有权限,是什么问题呢?该加的权限我都加了啊 ,路径是 /storage/emulated/0/downloads/xxx.mp4,您看看哪里不对?我是米6手机
    xiaoyanger:是能加进去的,只是在滑动的时候外层新建的窗口会出现跟不上滑动的情况,特别是快速滑动,此时有视频正在播放的话,VideoView在ScrollView滑动时会出现抖动的现象。另外VideoView的一些View属性不受控制可以试一下,比如设置onClickListener的点击监听都不会有效果的。
  • 23422c267505:赞赏了一下,的确用到了,感谢
    xiaoyanger: @_vvvvvc 谢谢。😃
  • 微微心凉L:楼主我在夜神使用该demo 小窗口之后 在全屏爆如下错误
    java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
    在NiceViedeoPlayer.java 399 行
    xiaoyanger:@微微心凉L 客气:smiley:
    微微心凉L:@xiaoyanger :smile: 学习了 。 感谢楼主
    xiaoyanger:@微微心凉L hello,目前没有支持直接从小窗口切换到全屏哦,你可以改动一下源码,从小窗口切换到全屏之前要先将mContainer从contentView移除,重新添加(宽高改为屏幕大小)到contentView并横屏。
  • 29ad9853a3f1:有问题啊,老铁,竖屏的时候切换全屏的时候只是变成横评而已,下面button还在,视频没有全屏
    xiaoyanger:@29ad9853a3f1 这样啊:sweat:
    29ad9853a3f1:@xiaoyanger 我的问题,我忘记在mainfrest加个配置,切换的时候生命周期方法重新走了,难怪
    xiaoyanger: @29ad9853a3f1 不会吧,你是什么机型测试的呢?
  • PeanutZYH:当视频在播放时,跳到第二个页面后,第一个页面的视频会还在播放,同样在第二个页面若播放视频,按back键返回第一个页面时, 几秒钟后,会响起之前的视频声音,,希望把细节处理好些,对于小白来不知道如何处理呢
  • looooker:厉害了,老铁
    xiaoyanger: @looooker ☺
  • FynnJason:办公室小野,我好朋友他们公司里的。:joy:
    xiaoyanger: @FynnJason 网上找的视频,哈哈😄
  • dee1ae6989b4:我是个新手,我想把你的代码转换成KOTLIN,并在GITHUB上可否。
    xiaoyanger:@伟大的小炮殿下 :flushed:
    伟大的小炮殿下:@xiaoyanger 这哥想问的应该不是可不可以转换,而是放在Github上
    xiaoyanger:@鱼v缸 应该是可以的,Kotlin我也才开始学习
  • 水天相映:曾经碰到过popupwindows 里面用surface view 不显示画面的问题 后来一样的代码放activity 里面就播放正常 楼主知道是什么原因吗
  • layjoy:MoboPlayer做的小窗口播放就不错,全屏游戏界面也可以播放视频,其他类型的app是不是也可以利用SurfaceView实现分屏效果?
  • 东方末明:赞,楼主写的很好,学习了,希望楼主多写!
    xiaoyanger: @东方末明 谢谢。😁
  • 古古有灵:一言不合贴代码,这就没法看了
    醉酒肆之:哈哈哈
  • 艾伦oy:写的不错
  • 无心下棋:不知道楼主有没有发现。在小米手机上,视频开始缓存之后,还没开始播这个时间内。退出播放的页面。应用就卡死了。
    a9c99f65dd58:@无心下棋 在小米3s上遇到过这个问题,这是一个异常行为引起的:当前界面失去焦点后,TextureView会回调onSurfaceTextureDestroyed()
    无心下棋: @xiaoyanger 小米3,mediaplayer的那个reset方法挂了
    xiaoyanger:我这边用MI 5测试时没发现这个问题呢,方便告知一下你是小米那个型号呢,还有就是有没有报什么错误信息呢,谢谢!

本文标题:用MediaPlayer+TextureView封装一个完美实现

本文链接:https://www.haomeiwen.com/subject/tfijtxtx.html