文章目录

  • android 视频播放器框架 [饺子播放器](https://github.com/Jzvd/JiaoZiVideoPlayer) 源码解析与评估
    • 前言
    • Library 主要类
    • 横竖屏切换
    • 传感器监听
    • 视频控制条触摸实现
    • Gif的生成
    • 播放引擎策略实现
    • 其他功能亮点
    • Jzvd播放界面 主实现类
    • 评估

android 视频播放器框架 饺子播放器 源码解析与评估


前言

使用的源码版本为7.3.0,饺子播放器源码设计精良,是个狠人,非常值得学习,在看源码的过程中觉得有些地方非常值得记录一下;

代码挺多,本着不求甚解的解读原则,把代码中的亮点或者常用功能点的代码详细摘录下来;


Library 主要类

JZDataSource 配置类;

主要是包括 视频播放配置,播放列表是否续播等数据;

public static final String URL_KEY_DEFAULT = "URL_KEY_DEFAULT";public int currentUrlIndex; public LinkedHashMap urlsMap = new LinkedHashMap();public String title = ""; //配置titlepublic HashMap<String, String> headerMap = new HashMap<>();public boolean looping = false; //播放全局配置;public Object[] objects;

JZMediaInterface 播放引擎抽象

作为可切换播放引擎的抽象, 使用TextureView 作为显示容器;

xxx implements TextureView.SurfaceTextureListenerpublic static SurfaceTexture SAVED_SURFACE; //textureture的显示参数;//使用发送消息到子线程的handerThread;public HandlerThread mMediaHandlerThread; //绑定handerThread的handler,发送消息到子线程的消息队列;public Handler mMediaHandler;//发送消息到主线程的消息队列;public Handler handler;public Jzvd jzvd;

JZMediaSystem 使用系统默认播放引擎实现

该类作为MediaPlayer的代理,使用系统提供的MediaPlayer作为视频的播放;

xxx extends JZMediaInterface//prepare方法;@Overridepublic void prepare() {release();//新建子线程HandlerThread 搭配 Handler,发送消息至子线程的消息队列;mMediaHandlerThread = new HandlerThread("JZVD");mMediaHandlerThread.start();mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里//主线程建立的Handler,发送至主线程的消息队列;handler = new Handler();mMediaHandler.post(() -> {//sub-thread 设置默认参数try {mediaPlayer = new MediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setLooping(jzvd.jzDataSource.looping);mediaPlayer.setOnPreparedListener(JZMediaSystem.this);mediaPlayer.setOnCompletionListener(JZMediaSystem.this);mediaPlayer.setOnBufferingUpdateListener(JZMediaSystem.this);mediaPlayer.setScreenOnWhilePlaying(true);mediaPlayer.setOnSeekCompleteListener(JZMediaSystem.this);mediaPlayer.setOnErrorListener(JZMediaSystem.this);mediaPlayer.setOnInfoListener(JZMediaSystem.this);mediaPlayer.setOnVideoSizeChangedListener(JZMediaSystem.this);//调用Mediaplayer的setDataSource方法设置视频源;Class<MediaPlayer> clazz = MediaPlayer.class;Method method = clazz.getDeclaredMethod("setDataSource", String.class, Map.class);method.invoke(mediaPlayer, jzvd.jzDataSource.getCurrentUrl().toString(), jzvd.jzDataSource.headerMap);mediaPlayer.prepareAsync();mediaPlayer.setSurface(new Surface(SAVED_SURFACE));} catch (Exception e) {e.printStackTrace();}});}//release@Overridepublic void release() {//not perfect change you laterif (mMediaHandler != null && mMediaHandlerThread != null && mediaPlayer != null) {//不知道有没有妖孽HandlerThread tmpHandlerThread = mMediaHandlerThread;MediaPlayer tmpMediaPlayer = mediaPlayer;JZMediaInterface.SAVED_SURFACE = null;mMediaHandler.post(() -> {tmpMediaPlayer.setSurface(null);tmpMediaPlayer.release();tmpHandlerThread.quit();});mediaPlayer = null;}}//在texture 建立时生成surfaceTexture后,与mediaplayer建立连接;@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {if (SAVED_SURFACE == null) {SAVED_SURFACE = surface;prepare();} else {jzvd.textureView.setSurfaceTexture(SAVED_SURFACE);}}

JZTextureView 自定义视频显示界面;

采用Android系统的VideoView的onMeasure方法;支持VIDEO_IMAGE_DISPLAY_TYPE_ADAPTER (默认情况), VIDEO_IMAGE_DISPLAY_TYPE_FILL_PARENT(拉伸全屏), VIDEO_IMAGE_DISPLAY_TYPE_FILL_SCROP (crop 裁剪至全屏) VIDEO_IMAGE_DISPLAY_TYPE_ORIGINAL (原图大小) 四种模式;

 @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {Log.i(TAG, "onMeasure " + " [" + this.hashCode() + "] ");int viewRotation = (int) getRotation();//原图大小,onVideoSizeChanged 返回大小;int videoWidth = currentVideoWidth;int videoHeight = currentVideoHeight;//父view大小,jzvd的大小;int parentHeight = ((View) getParent()).getMeasuredHeight();int parentWidth = ((View) getParent()).getMeasuredWidth();if (parentWidth != 0 && parentHeight != 0 && videoWidth != 0 && videoHeight != 0) {//全屏设置模式: if (Jzvd.VIDEO_IMAGE_DISPLAY_TYPE == Jzvd.VIDEO_IMAGE_DISPLAY_TYPE_FILL_PARENT) {//如果是90度或者270度时,交换父view宽高,设置预览页面大小;if (viewRotation == 90 || viewRotation == 270) {int tempSize = parentWidth;parentWidth = parentHeight;parentHeight = tempSize;}/**强制充满**/videoHeight = videoWidth * parentHeight / parentWidth;}}// 如果判断成立,则说明显示的TextureView和本身的位置是有90度的旋转的,所以需要交换宽高参数。if (viewRotation == 90 || viewRotation == 270) {int tempMeasureSpec = widthMeasureSpec;widthMeasureSpec = heightMeasureSpec;heightMeasureSpec = tempMeasureSpec;}// 获取不同模式下的测量高度   int width = getDefaultSize(videoWidth, widthMeasureSpec);int height = getDefaultSize(videoHeight, heightMeasureSpec);//在传入原图大小后的调整;if (videoWidth > 0 && videoHeight > 0) {int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);Log.i(TAG, "widthMeasureSpec  [" + MeasureSpec.toString(widthMeasureSpec) + "]");Log.i(TAG, "heightMeasureSpec [" + MeasureSpec.toString(heightMeasureSpec) + "]");//如果宽高参数都设置的是精准模式if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {// the size is fixedwidth = widthSpecSize;height = heightSpecSize;// for compatibility, we adjust size based on aspect ratioif (videoWidth * height < width * videoHeight) {//测量的宽高比 > 原图的宽高比,调整测量的宽度;width = height * videoWidth / videoHeight;} else if (videoWidth * height > width * videoHeight) {height = width * videoHeight / videoWidth;}} else if (widthSpecMode == MeasureSpec.EXACTLY) {// only the width is fixed, adjust the height to match aspect ratio if possiblewidth = widthSpecSize;height = width * videoHeight / videoWidth;//在高度为自适应且调整后的高度大于测量的高度,则高度固定,调整宽度;if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {// couldn't match aspect ratio within the constraintsheight = heightSpecSize;width = height * videoWidth / videoHeight;}} else if (heightSpecMode == MeasureSpec.EXACTLY) {// only the height is fixed, adjust the width to match aspect ratio if possibleheight = heightSpecSize;width = height * videoWidth / videoHeight;if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {// couldn't match aspect ratio within the constraintswidth = widthSpecSize;height = width * videoHeight / videoWidth;}} else {// neither the width nor the height are fixed, try to use actual video sizewidth = videoWidth;height = videoHeight;if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {// too tall, decrease both width and heightheight = heightSpecSize;width = height * videoWidth / videoHeight;}if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {// too wide, decrease both width and heightwidth = widthSpecSize;height = width * videoHeight / videoWidth;}}} else {// no size yet, just adopt the given spec sizes}if (parentWidth != 0 && parentHeight != 0 && videoWidth != 0 && videoHeight != 0) {//原图设置模式if (Jzvd.VIDEO_IMAGE_DISPLAY_TYPE == Jzvd.VIDEO_IMAGE_DISPLAY_TYPE_ORIGINAL) {/**原图**/height = videoHeight;width = videoWidth;} else if (Jzvd.VIDEO_IMAGE_DISPLAY_TYPE == Jzvd.VIDEO_IMAGE_DISPLAY_TYPE_FILL_SCROP) {//裁切设置模式if (viewRotation == 90 || viewRotation == 270) {int tempSize = parentWidth;parentWidth = parentHeight;parentHeight = tempSize;}/**充满剪切**/if (((double) videoHeight / videoWidth) > ((double) parentHeight / parentWidth)) {height = (int) (((double) parentWidth / (double) width * (double) height));width = parentWidth;} else if (((double) videoHeight / videoWidth) < ((double) parentHeight / parentWidth)) {width = (int) (((double) parentHeight / (double) height * (double) width));height = parentHeight;}}}setMeasuredDimension(width, height);}

JZUtils 工具类

包括 时间格式化,wifi检测,设置横竖屏,本地数据保存,状态栏工具,宽高工具等;

Jzvd 抽象播放界面

抽象类,主要功能实现类; 对视频功能的一系列封装,可自由继承此类,扩展视频功能和ui; 组合其他类的设计, 外观设计模式(或者中介者?);

  • 状态常量
屏幕状态public static final int SCREEN_NORMAL = 0; //普通播放状态public static final int SCREEN_FULLSCREEN = 1; //全屏播放状态;public static final int SCREEN_TINY = 2; //小屏播放状态;播放状态public static final int STATE_IDLE = -1; //空闲状态,构造布局init方法后public static final int STATE_NORMAL = 0; //设置参数后的状态;public static final int STATE_PREPARING = 1;//准备public static final int STATE_PREPARING_CHANGE_URL = 2;//改变资源,清晰度public static final int STATE_PREPARING_PLAYING = 3;//准备播放bufferpublic static final int STATE_PREPARED = 4;//准备完毕public static final int STATE_PLAYING = 5;//播放public static final int STATE_PAUSE = 6;//暂停public static final int STATE_AUTO_COMPLETE = 7;//完成public static final int STATE_ERROR = 8;//错误//视频显示类型public static final int VIDEO_IMAGE_DISPLAY_TYPE_ADAPTER = 0;//DEFAULTpublic static final int VIDEO_IMAGE_DISPLAY_TYPE_FILL_PARENT = 1;//拉伸public static final int VIDEO_IMAGE_DISPLAY_TYPE_FILL_SCROP = 2;//裁切public static final int VIDEO_IMAGE_DISPLAY_TYPE_ORIGINAL = 3;//原图...
  • 设置资源,等待播放;

主要设置参数: screen屏幕状态,JZDataSource封装数据,mediaInterfaceClass播放引擎策略;

主要关键点:

1.static 容器列表

public static LinkedList<ViewGroup> CONTAINER_LIST = new LinkedList<>();
主要用于判断横竖屏和屏幕转化的判断;

2.static 静态强引用的播放主类

public static Jzvd CURRENT_JZVD; 可自由控制播放实现类;

3.播放参数对象

public JZDataSource jzDataSource;

4.播放引擎使用类名作为策略

//使用反射生成播放引擎 策略类;public Class mediaInterfaceClass;public JZMediaInterface mediaInterface;

5.播放视图view

public JZTextureView textureView; 使用textureView承载播放视频, 后期可使用surfaceView代替;

6.使用TimeTask获取progress

protected ProgressTimerTask mProgressTimerTask; 实现进度播放;

7.统一销毁jzvd播放类

public static void releaseAllVideos() {Log.d(TAG, "releaseAllVideos");if (CURRENT_JZVD != null) {CURRENT_JZVD.reset();CURRENT_JZVD = null;}}public void reset() {Log.i(TAG, "reset " + " [" + this.hashCode() + "] ");//销毁前保存当前播放进度;if (state == STATE_PLAYING || state == STATE_PAUSE) {long position = getCurrentPositionWhenPlaying();JZUtils.saveProgress(getContext(), jzDataSource.getCurrentUrl(), position);}cancelProgressTimer();dismissBrightnessDialog();dismissProgressDialog();dismissVolumeDialog();onStateNormal();//播放视图view的容器,TextureView or SurfaceView;textureViewContainer.removeAllViews();//audio解绑;AudioManager mAudioManager = (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);mAudioManager.abandonAudioFocus(onAudioFocusChangeListener);JZUtils.scanForActivity(getContext()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//播放引擎的释放;if (mediaInterface != null) mediaInterface.release();}

8.返回操作

public static boolean backPress() {Log.i(TAG, "backPress");if (CONTAINER_LIST.size() != 0 && CURRENT_JZVD != null) {//判断条件,因为当前所有goBack都是回到普通窗口CURRENT_JZVD.gotoScreenNormal();return true;} else if (CONTAINER_LIST.size() == 0 && CURRENT_JZVD != null && CURRENT_JZVD.screen != SCREEN_NORMAL) {//退出直接进入的全屏CURRENT_JZVD.clearFloatScreen();return true;}return false;}

9.播放操作判断;

//参数合法性判断;normal(setup)下,判断是否需要弹wifi提示;private void clickStart() {Log.i(TAG, "onClick start [" + this.hashCode() + "] ");if (jzDataSource == null || jzDataSource.urlsMap.isEmpty() || jzDataSource.getCurrentUrl() == null) {Toast.makeText(getContext(), getResources().getString(R.string.no_url), Toast.LENGTH_SHORT).show();return;}if (state == STATE_NORMAL) {if (!jzDataSource.getCurrentUrl().toString().startsWith("file") && !jzDataSource.getCurrentUrl().toString().startsWith("/") &&!JZUtils.isWifiConnected(getContext()) && !WIFI_TIP_DIALOG_SHOWED) {//这个可以放到std中showWifiDialog();return;}startVideo();} else if (state == STATE_PLAYING) {Log.d(TAG, "pauseVideo [" + this.hashCode() + "] ");mediaInterface.pause();onStatePause();} else if (state == STATE_PAUSE) {mediaInterface.start();onStatePlaying();} else if (state == STATE_AUTO_COMPLETE) {startVideo();}}//开始播放视频;public void startVideo() {Log.d(TAG, "startVideo [" + this.hashCode() + "] ");//static变量引用当前jzvd;setCurrentJzvd(this);try {//播放引擎构建实现类对象;Constructor<JZMediaInterface> constructor = mediaInterfaceClass.getConstructor(Jzvd.class);this.mediaInterface = constructor.newInstance(this);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}//添加textureView后,进行prepare操作;addTextureView();mAudioManager = (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);mAudioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);JZUtils.scanForActivity(getContext()).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);//改变状态;onStatePreparing();}//jzvd(FrameLayout)中提供的视图View的容器;public void addTextureView() {Log.d(TAG, "addTextureView [" + this.hashCode() + "] ");if (textureView != null) textureViewContainer.removeView(textureView);textureView = new JZTextureView(getContext().getApplicationContext());//JZMediaInterface 实现 TextureView.SurfaceTextureListener ,当textureView添加到容器中,会调用available方法,初始化成功可进行prepare操作;textureView.setSurfaceTextureListener(mediaInterface);FrameLayout.LayoutParams layoutParams =new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT,Gravity.CENTER);textureViewContainer.addView(textureView, layoutParams);}public interface SurfaceTextureListener {void onSurfaceTextureAvailable(SurfaceTexture var1, int var2, int var3);void onSurfaceTextureSizeChanged(SurfaceTexture var1, int var2, int var3);boolean onSurfaceTextureDestroyed(SurfaceTexture var1);void onSurfaceTextureUpdated(SurfaceTexture var1);}    

当textureView 添加到view中,attached window后,会调用 onSurfaceTextureAvailable方法;

横竖屏切换

点击直接全屏播放功能;

 //添加到当前activity 的 decorview中;public static void startFullscreenDirectly(Context context, Class _class, JZDataSource jzDataSource) {//设置全屏flag;JZUtils.hideStatusBar(context);//设置屏幕方向;JZUtils.setRequestedOrientation(context, FULLSCREEN_ORIENTATION);//设置沉浸式状态栏;JZUtils.hideSystemUI(context);//获取当前activity的Decorview;ViewGroup vp = (ViewGroup) JZUtils.scanForActivity(context).getWindow().getDecorView();try {//获取播放Jzvd实现类的构造器;Constructor<Jzvd> constructor = _class.getConstructor(Context.class);//构建Jzvd实现类对象;final Jzvd jzvd = constructor.newInstance(context);FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);//提交到decorView中,覆盖于原页面上;vp.addView(jzvd, lp);//设置jzvd主要参数并播放;jzvd.setUp(jzDataSource, JzvdStd.SCREEN_FULLSCREEN);jzvd.startVideo();} catch (InstantiationException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}

横竖屏

CONTAINER_LIST: 手动全屏时 Jzvd parentView 会remove当前jzvd, 并添加parentView到此容器.
在恢复竖屏时会使用保存的parentView将remove的jzvd重新添加回来,达到横竖屏切换的目的;

//全屏,添加到jzvd自定义view的父View中;public void gotoScreenFullscreen() {gotoFullscreenTime = System.currentTimeMillis();jzvdContext = ((ViewGroup) getParent()).getContext();//自定义播放View 的直接父类,一般会套一个LinearLayout;ViewGroup vg = (ViewGroup) getParent();vg.removeView(this);cloneAJzvd(vg);CONTAINER_LIST.add(vg);vg = (ViewGroup) (JZUtils.scanForActivity(jzvdContext)).getWindow().getDecorView();vg.addView(this, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));//全屏flag,屏幕方向,沉浸式状态栏;setScreenFullscreen();JZUtils.hideStatusBar(jzvdContext);JZUtils.setRequestedOrientation(jzvdContext, FULLSCREEN_ORIENTATION);JZUtils.hideSystemUI(jzvdContext);//华为手机和有虚拟键的手机全屏时可隐藏虚拟键 issue:1326}// 重新clone Jzvd对象,用于全屏jzvd在偶然错误情况下返回没有播放视频的容器了;public void cloneAJzvd(ViewGroup vg) {try {//获取当前实现类构造器,new对象,设置原参数;Constructor<Jzvd> constructor = (Constructor<Jzvd>) Jzvd.this.getClass().getConstructor(Context.class);Jzvd jzvd = constructor.newInstance(getContext());jzvd.setId(getId());vg.addView(jzvd);jzvd.setUp(jzDataSource.cloneMe(), SCREEN_NORMAL, mediaInterfaceClass);} catch ...}//全屏恢复,添加原先remove的jzvd;public void gotoScreenNormal() {//goback本质上是gotogobakFullscreenTime = System.currentTimeMillis();//退出全屏//先尝试去除添加在decorview中的jzvd;ViewGroup vg = (ViewGroup) (JZUtils.scanForActivity(jzvdContext)).getWindow().getDecorView();vg.removeView(this);//取上次全屏时添加的vg引用,就是jzvd的父View,然后去除clone的jzvd,并添加当前的jzvd;CONTAINER_LIST.getLast().removeAllViews();CONTAINER_LIST.getLast().addView(this, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));//弹出上次添加的jzvd的parentView;CONTAINER_LIST.pop();setScreenNormal();//这块可以放到jzvd中JZUtils.showStatusBar(jzvdContext);JZUtils.setRequestedOrientation(jzvdContext, NORMAL_ORIENTATION);JZUtils.showSystemUI(jzvdContext);}

传感器监听

ScreenRotateUtils 加速度传感器,通过手机的转动控制播放器的横竖屏;

//传感器的监听;class OrientationSensorListener implements SensorEventListener {@Overridepublic void onSensorChanged(SensorEvent event) {float[] values = event.values;int orientation = ORIENTATION_UNKNOWN;float x = -values[DATA_X];orientationDirection = -x;float y = -values[DATA_Y];float z = -values[DATA_Z];float magnitude = x * x + y * y;if (magnitude * 4 >= z * z) {float oneEightyOverPi = 57.29577957855f;float angle = (float) (Math.atan2(-y, x) * oneEightyOverPi);orientation = 90 - Math.round(angle);// normalize to 0 - 359 rangewhile (orientation >= 360) {orientation -= 360;}while (orientation < 0) {orientation += 360;}}/*** 获取手机系统的重力感应开关设置,这段代码看需求,不要就删除* screenchange = 1 表示开启,screenchange = 0 表示禁用* 要是禁用了就直接返回*/if (isEffectSysSetting) {try {int isRotate = Settings.System.getInt(mActivity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION);// 如果用户禁用掉了重力感应就直接returnif (isRotate == 0) {return;}} catch (Settings.SettingNotFoundException e) {e.printStackTrace();}// 判断是否要进行中断信息传递if (!isOpenSensor) {return;}changeListener.orientationChange(orientation);}}@Overridepublic void onAccuracyChanged(Sensor sensor, int i) {}}

视频控制条触摸实现

jzvd 横屏下实现了3种dialog弹框控制视频;

  • 快进/快退左右滑动 dialog;
  • 左半部分上下滑动 dialog;
  • 右半不不封上下滑动 dialog;
//touch move 事件;private void touchActionMove(float x, float y) {Log.i(TAG, "onTouch surfaceContainer actionMove [" + this.hashCode() + "] ");float deltaX = x - mDownX;float deltaY = y - mDownY;float absDeltaX = Math.abs(deltaX);float absDeltaY = Math.abs(deltaY);if (screen == SCREEN_FULLSCREEN) {//拖动的是NavigationBar和状态栏if (mDownX > JZUtils.getScreenWidth(getContext()) || mDownY < JZUtils.getStatusBarHeight(getContext())) {return;}if (!mChangePosition && !mChangeVolume && !mChangeBrightness) {if (absDeltaX > THRESHOLD || absDeltaY > THRESHOLD) {cancelProgressTimer();//x方向移动距离触发mChangePosition flag, 显示快进/快退dialog;if (absDeltaX >= THRESHOLD) {// 全屏模式下的CURRENT_STATE_ERROR状态下,不响应进度拖动事件.// 否则会因为mediaplayer的状态非法导致App Crashif (state != STATE_ERROR) {mChangePosition = true;mGestureDownPosition = getCurrentPositionWhenPlaying();}} else {//如果y轴滑动距离超过设置的处理范围,那么进行滑动事件处理if (mDownX < mScreenWidth * 0.5f) {//左侧改变亮度mChangeBrightness = true;WindowManager.LayoutParams lp = JZUtils.getWindow(getContext()).getAttributes();if (lp.screenBrightness < 0) {try {mGestureDownBrightness = Settings.System.getInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);Log.i(TAG, "current system brightness: " + mGestureDownBrightness);} catch (Settings.SettingNotFoundException e) {e.printStackTrace();}} else {mGestureDownBrightness = lp.screenBrightness * 255;Log.i(TAG, "current activity brightness: " + mGestureDownBrightness);}} else {//右侧改变声音mChangeVolume = true;mGestureDownVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);}}}}}//一旦进入一个dialog 模式,就一直改变这个dialog;if (mChangePosition) {long totalTimeDuration = getDuration();mSeekTimePosition = (int) (mGestureDownPosition + deltaX * totalTimeDuration / mScreenWidth);if (mSeekTimePosition > totalTimeDuration)mSeekTimePosition = totalTimeDuration;String seekTime = JZUtils.stringForTime(mSeekTimePosition);String totalTime = JZUtils.stringForTime(totalTimeDuration);showProgressDialog(deltaX, seekTime, mSeekTimePosition, totalTime, totalTimeDuration);}if (mChangeVolume) {deltaY = -deltaY;int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);int deltaV = (int) (max * deltaY * 3 / mScreenHeight);mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mGestureDownVolume + deltaV, 0);//dialog中显示百分比int volumePercent = (int) (mGestureDownVolume * 100 / max + deltaY * 3 * 100 / mScreenHeight);showVolumeDialog(-deltaY, volumePercent);}if (mChangeBrightness) {deltaY = -deltaY;int deltaV = (int) (255 * deltaY * 3 / mScreenHeight);WindowManager.LayoutParams params = JZUtils.getWindow(getContext()).getAttributes();if (((mGestureDownBrightness + deltaV) / 255) >= 1) {//这和声音有区别,必须自己过滤一下负值params.screenBrightness = 1;} else if (((mGestureDownBrightness + deltaV) / 255) <= 0) {params.screenBrightness = 0.01f;} else {params.screenBrightness = (mGestureDownBrightness + deltaV) / 255;}JZUtils.getWindow(getContext()).setAttributes(params);//dialog中显示百分比int brightnessPercent = (int) (mGestureDownBrightness * 100 / 255 + deltaY * 3 * 100 / mScreenHeight);showBrightnessDialog(brightnessPercent);
//                        mDownY = y;}}

Gif的生成

GifCreateHelper: 创建Gif,使用FFmpeg 截取视频中的图片,然后拼接成gif;

AnimatedGifEncoder : 使用bitmaps生成一张gif; (这个类太长了,就不放了,有兴趣的可以在github中查看)

 //需要引入ffmpeg的包;implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-core:1.0.15'implementation 'com.github.wseemann:FFmpegMediaMetadataRetriever-native:1.0.15'/*** @param bitmapFromTime gif图在视频中的开始时间* @param vedioUrl       视频链接*/public void startGif(long bitmapFromTime, String vedioUrl) {//设置gif的时长和帧间隔时长,计算总共需要截取的张数;int bitmapCount = mGifPeriod / mDelay;String[] picList = new String[bitmapCount];isDownloadComplete = false;FFmpegMediaMetadataRetriever mmr = prepareFFmpegMediaMetadataRetriever(vedioUrl);for (int i = 0; i < bitmapCount; i++) {final int index = i;//`ExecutorService executorService = Executors.newCachedThreadPool();` 使用多个非核心线程池加载;executorService.submit(new Runnable() {@Overridepublic void run() {//先缓存到本地,全放入内存占用空间太大String path = saveBitmap(mmr.getScaledFrameAtTime((bitmapFromTime + index * mDelay)*1000,FFmpegMediaMetadataRetriever.OPTION_CLOSEST,gifWidth,gifHeight),cacheImageDir + "/" + System.currentTimeMillis() + "index-" + index + ".png");boolean isCurrentSuccess = true;if (!TextUtils.isEmpty(path)) {picList[index] = path;} else {//标志错误的string;picList[index] = completeButNoImageTag;isCurrentSuccess = false;}checkCompleteAndDoNext(picList, isCurrentSuccess);if(isDownloadComplete){mmr.release();}}});}}private FFmpegMediaMetadataRetriever prepareFFmpegMediaMetadataRetriever(String vedioUrl){FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();mmr.setDataSource(vedioUrl);mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_ALBUM);mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_ARTIST);return mmr;}private void checkCompleteAndDoNext(String[] picList, boolean isCurrentSuccess) {//线程安全;synchronized (GifCreateHelper.class) {if (isDownloadComplete) {return;}if (picList == null || picList.length == 0) {combinePicToGif(picList);}int emptyCount = 0;for (String path : picList) {if (TextUtils.isEmpty(path)) {emptyCount++;}}//listener 显示进度;mJzGifListener.process(picList.length - emptyCount, picList.length, isCurrentSuccess ? "下载成功" : "下载失败");if (emptyCount == 0) {isDownloadComplete = true;mPlayer.post(new Runnable() {@Overridepublic void run() {combinePicToGif(picList);}});}}}private void combinePicToGif(String[] picList) {//设置输出流;File gifFile = ensureFile(new File(mGifPath));ArrayList<String> rightPic = new ArrayList<>();for (String picItem : picList) {if (!TextUtils.isEmpty(picItem) && !completeButNoImageTag.equals(picItem)) {rightPic.add(picItem);}}if (rightPic.size() > 2) {if (createGif(gifFile, rightPic, mDelay, mSampleSize, mSmallScale)) {mJzGifListener.result(true, gifFile);} else {mJzGifListener.result(false, null);}} else {mJzGifListener.result(false, null);}deleteDirWihtFile(new File(cacheImageDir));//清除缓存的图片}//create gif (AnimatedGifEncoder)核心方法/*** 生成gif图** @param file         保存的文件路径,请确保文件夹目录已经创建* @param pics         需要转化的bitmap本地路径集合* @param delay        每一帧之间的延时* @param inSampleSize 采样率,最小值1 即:每隔inSampleSize个像素点,取一个读入到内存。越大处理越快* @param smallScale   缩小倍数,越大处理越快*/public boolean createGif(File file, List<String> pics, int delay, int inSampleSize, int smallScale) {ByteArrayOutputStream baos = new ByteArrayOutputStream();AnimatedGifEncoder localAnimatedGifEncoder = new AnimatedGifEncoder();localAnimatedGifEncoder.start(baos);localAnimatedGifEncoder.setRepeat(0);//设置生成gif的开始播放时间。0为立即开始播放localAnimatedGifEncoder.setDelay(delay);for (int i = 0; i < pics.size(); i++) {BitmapFactory.Options options = new BitmapFactory.Options();options.inSampleSize = inSampleSize;options.inJustDecodeBounds = true; // 先获取原大小BitmapFactory.decodeFile(pics.get(i), options);double w = (double) options.outWidth / smallScale;double h = (double) options.outHeight / smallScale;options.inJustDecodeBounds = false; // 获取新的大小Bitmap bitmap = BitmapFactory.decodeFile(pics.get(i), options);Bitmap pic = ThumbnailUtils.extractThumbnail(bitmap, (int) w, (int) h);localAnimatedGifEncoder.addFrame(pic);bitmap.recycle();pic.recycle();mJzGifListener.process(i, pics.size(), "组合中");}localAnimatedGifEncoder.finish();//finishtry {FileOutputStream fos = new FileOutputStream(file.getPath());baos.writeTo(fos);baos.flush();fos.flush();baos.close();fos.close();} catch (IOException e) {e.printStackTrace();return false;}return true;}

播放引擎策略实现

暂时有 JZMediaSystem,JZMediaExo,JZMediaIjk,JZMediaSystemAssertFolder 四种播放引擎,其实是3种,JZMediaSystemAssertFolder只是数据源不同;

抽取播放常用接口,通过textureView attach 到window后 调用 onSurfaceTextureAvailable进行音视频的prepare过程,其中使用四种具体策略抽象出播放引擎接口;

  • JZMediaSystem

主要使用 public MediaPlayer mediaPlayer; 作为音视频播放器; 核心是prepare过程;

//使用 HandlerThread 搭配 Handler 进行子线程下的mediaplayer的初始化过程;public void prepare() {release();mMediaHandlerThread = new HandlerThread("JZVD");mMediaHandlerThread.start();mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里handler = new Handler();mMediaHandler.post(() -> {try {mediaPlayer = new MediaPlayer();mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mediaPlayer.setLooping(jzvd.jzDataSource.looping);mediaPlayer.setOnPreparedListener(JZMediaSystem.this);mediaPlayer.setOnCompletionListener(JZMediaSystem.this);mediaPlayer.setOnBufferingUpdateListener(JZMediaSystem.this);mediaPlayer.setScreenOnWhilePlaying(true);mediaPlayer.setOnSeekCompleteListener(JZMediaSystem.this);mediaPlayer.setOnErrorListener(JZMediaSystem.this);mediaPlayer.setOnInfoListener(JZMediaSystem.this);mediaPlayer.setOnVideoSizeChangedListener(JZMediaSystem.this);Class<MediaPlayer> clazz = MediaPlayer.class;//通过反射方式调用setDataSource设置数据源;Method method = clazz.getDeclaredMethod("setDataSource", String.class, Map.class);method.invoke(mediaPlayer, jzvd.jzDataSource.getCurrentUrl().toString(), jzvd.jzDataSource.headerMap);mediaPlayer.prepareAsync();mediaPlayer.setSurface(new Surface(SAVED_SURFACE));} catch (Exception e) {e.printStackTrace();}});}JZMediaSystemAssertFolder 不同于数据源设置不同;//two lines are differentAssetFileDescriptor assetFileDescriptor = jzvd.getContext().getAssets().openFd(jzvd.jzDataSource.getCurrentUrl().toString());mediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(), assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());
  • JZMediaExo
public void prepare() {Log.e(TAG, "prepare");Context context = jzvd.getContext();release();mMediaHandlerThread = new HandlerThread("JZVD");mMediaHandlerThread.start();mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里handler = new Handler();mMediaHandler.post(() -> {BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();TrackSelection.Factory videoTrackSelectionFactory =new AdaptiveTrackSelection.Factory(bandwidthMeter);TrackSelector trackSelector =new DefaultTrackSelector(videoTrackSelectionFactory);LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),360000, 600000, 1000, 5000,C.LENGTH_UNSET,false);// 2. Create the playerRenderersFactory renderersFactory = new DefaultRenderersFactory(context);simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl);// Produces DataSource instances through which media data is loaded.DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,Util.getUserAgent(context, context.getResources().getString(R.string.app_name)));String currUrl = jzvd.jzDataSource.getCurrentUrl().toString();MediaSource videoSource;if (currUrl.contains(".m3u8")) {videoSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(currUrl), handler, null);} else {videoSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(currUrl));}simpleExoPlayer.addVideoListener(this);Log.e(TAG, "URL Link = " + currUrl);simpleExoPlayer.addListener(this);Boolean isLoop = jzvd.jzDataSource.looping;if (isLoop) {simpleExoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);} else {simpleExoPlayer.setRepeatMode(Player.REPEAT_MODE_OFF);}simpleExoPlayer.prepare(videoSource);simpleExoPlayer.setPlayWhenReady(true);callback = new onBufferingUpdate();if (jzvd.textureView != null) {SurfaceTexture surfaceTexture = jzvd.textureView.getSurfaceTexture();if (surfaceTexture != null) {simpleExoPlayer.setVideoSurface(new Surface(surfaceTexture));}}});}
  • JZMediaIjk
public void prepare() {release();mMediaHandlerThread = new HandlerThread("JZVD");mMediaHandlerThread.start();mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里handler = new Handler();mMediaHandler.post(() -> {ijkMediaPlayer = new IjkMediaPlayer();ijkMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);1为硬解 0为软解ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1);//使用opensles把文件从java层拷贝到native层ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);//视频格式ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);//跳帧处理(-1~120)。CPU处理慢时,进行跳帧处理,保证音视频同步ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);//0为一进入就播放,1为进入时不播放ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 0);域名检测ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);//设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);//最大缓冲大小,单位kbijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 1024 * 1024);//某些视频在SeekTo的时候,会跳回到拖动前的位置,这是因为视频的关键帧的问题,通俗一点就是FFMPEG不兼容,视频压缩过于厉害,seek只支持关键帧,出现这个情况就是原始的视频文件中i 帧比较少ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1);//是否重连ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);//http重定向httpsijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);//设置seekTo能够快速seek到指定位置并播放ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "fastseek");//播放前的探测Size,默认是1M, 改小一点会出画面更快ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 10);ijkMediaPlayer.setOnPreparedListener(JZMediaIjk.this);ijkMediaPlayer.setOnVideoSizeChangedListener(JZMediaIjk.this);ijkMediaPlayer.setOnCompletionListener(JZMediaIjk.this);ijkMediaPlayer.setOnErrorListener(JZMediaIjk.this);ijkMediaPlayer.setOnInfoListener(JZMediaIjk.this);ijkMediaPlayer.setOnBufferingUpdateListener(JZMediaIjk.this);ijkMediaPlayer.setOnSeekCompleteListener(JZMediaIjk.this);ijkMediaPlayer.setOnTimedTextListener(JZMediaIjk.this);try {ijkMediaPlayer.setDataSource(jzvd.jzDataSource.getCurrentUrl().toString());ijkMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);ijkMediaPlayer.setScreenOnWhilePlaying(true);ijkMediaPlayer.prepareAsync();ijkMediaPlayer.setSurface(new Surface(jzvd.textureView.getSurfaceTexture()));} catch (IOException e) {e.printStackTrace();}});}

其他功能亮点

AudioManager 播放焦点监听事件

 public static AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {//是否新建个class,代码更规矩,并且变量的位置也很尴尬@Overridepublic void onAudioFocusChange(int focusChange) {switch (focusChange) {case AudioManager.AUDIOFOCUS_GAIN:break;case AudioManager.AUDIOFOCUS_LOSS://长时间失去了Audio Focus,直接销毁;releaseAllVideos();Log.d(TAG, "AUDIOFOCUS_LOSS [" + this.hashCode() + "]");break;case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT://暂时失去了Audio Focus,是播放状态继续播放;try {Jzvd player = CURRENT_JZVD;if (player != null && player.state == Jzvd.STATE_PLAYING) {player.startButton.performClick();}} catch (IllegalStateException e) {e.printStackTrace();}Log.d(TAG, "AUDIOFOCUS_LOSS_TRANSIENT [" + this.hashCode() + "]");break;case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:break;}}};

wifi 网络状态监听

start 按钮点击会判断当前网络的状态,也是经常有的功能;

//手动注册wifi监听;public BroadcastReceiver wifiReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {boolean isWifi = JZUtils.isWifiConnected(context);if (mIsWifi == isWifi) return;mIsWifi = isWifi;if (!mIsWifi && !WIFI_TIP_DIALOG_SHOWED && state == STATE_PLAYING) {startButton.performClick(); //pauseshowWifiDialog();}}}};

电池电量监听

//广播接受者public BroadcastReceiver battertReceiver = new BroadcastReceiver() {public void onReceive(Context context, Intent intent) {String action = intent.getAction();if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {int level = intent.getIntExtra("level", 0);int scale = intent.getIntExtra("scale", 100);int percent = level * 100 / scale;LAST_GET_BATTERYLEVEL_PERCENT = percent;setBatteryLevel();try {getContext().unregisterReceiver(battertReceiver);} catch (Exception e) {e.printStackTrace();}}}};

视频清晰度切换

记录当前播放位置,更换JZDataSource播放数据,release 当前jzvd, 重新startVideo;

这里可以搭配另一个视频缓存库使用: androidvideocache github传送门

可参考我写的另一篇blog ,专门介绍这款 androidVideoCache 源码及设计思想 传送门

这个库也是非常值得学习的一个库;

protected void initData() {super.initData();LinkedHashMap map = new LinkedHashMap();//在本地缓存当前视频,高清的意思就是本地视频;String proxyUrl = ApplicationDemo.getProxy(mContext).getProxyUrl(Urls.videoUrls[0][9]);map.put("高清", proxyUrl);map.put("标清", Urls.videoUrls[0][6]);map.put("普清", Urls.videoUrlList[0]);JZDataSource jzDataSource = new JZDataSource(map, "饺子不信");jzDataSource.looping = true;jzDataSource.currentUrlIndex = 2;jzDataSource.headerMap.put("key", "value");//headermJzvdStd.setUp(jzDataSource, JzvdStd.SCREEN_NORMAL);Glide.with(this).load(Urls.videoPosterList[0]).into(mJzvdStd.posterImageView);}    

本地记录进度seek

 public void onStatePlaying() {Log.i(TAG, "onStatePlaying " + " [" + this.hashCode() + "] ");if (state == STATE_PREPARED) {//如果是准备完成视频后第一次播放,先判断是否需要跳转进度。//更换url时,记录上次播放位置,prepare后seek到此播放位置;if (seekToInAdvance != 0) {mediaInterface.seekTo(seekToInAdvance);seekToInAdvance = 0;} else {//reset时存储在本地的位置;long position = JZUtils.getSavedProgress(getContext(), jzDataSource.getCurrentUrl());if (position != 0) {mediaInterface.seekTo(position);//这里为什么区分开呢,第一次的播放和resume播放是不一样的。 这里怎么区分是一个问题。然后}}}state = STATE_PLAYING;startProgressTimer();}

双击播放与暂停

//doublClick 这两个全局变量只在ontouch中使用,就近放置便于阅读private long lastClickTime = 0;private long doubleTime = 200;private ArrayDeque<Runnable> delayTask = new ArrayDeque<>();//在jzvd 的 实现类 JzvdStd 中实现;public boolean onTouch(View v, MotionEvent event) {int id = v.getId();if (id == R.id.surface_container) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_MOVE:break;case MotionEvent.ACTION_UP:startDismissControlViewTimer();if (mChangePosition) {long duration = getDuration();int progress = (int) (mSeekTimePosition * 100 / (duration == 0 ? 1 : duration));bottomProgressBar.setProgress(progress);}//加上延时是为了判断点击是否是双击之一,双击不执行这个逻辑Runnable task = () -> {if (!mChangePosition && !mChangeVolume) {onClickUiToggle();}};v.postDelayed(task, doubleTime + 20);delayTask.add(task);//只要连续的两个up事件while (delayTask.size() > 2) {delayTask.pollFirst();}long currentTimeMillis = System.currentTimeMillis();if (currentTimeMillis - lastClickTime < doubleTime) {//若是连续的双击,则去除单击事件;for (Runnable taskItem : delayTask) {v.removeCallbacks(taskItem);}if (state == STATE_PLAYING || state == STATE_PAUSE) {Log.d(TAG, "doublClick [" + this.hashCode() + "] ");//实现连续双击控制视频的暂停和播放;startButton.performClick();}}lastClickTime = currentTimeMillis;break;}} else if (id == R.id.bottom_seek_progress) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:cancelDismissControlViewTimer();break;case MotionEvent.ACTION_UP:startDismissControlViewTimer();break;}}return super.onTouch(v, event);}

视频点击平滑到另一个页面播放 ListViewToDetailActivity

列表平滑进入详情页

1.获取当前播放的JZVD添加到详情页中;

2.获取列表中JZVD的坐标,宽高,获取详情页JZVD坐标,宽高,借助ViewMoveHelper实现平移;

现象是:

点击位于列表中的jzvd,平滑到另一个act的页面,jzvd继续播放,很多直播平台有这种效果;

重点原理是:

使用一个static的变量强引用了Jzvd 对象(此种思想也可以做全局播放,不过要注意内存泄露问题,需要控制释放), 上一个页面remove调当前jzvd,防止下一个页面的容器添加jzvd时出现父类已经有孩子 parent has a child之类的异常; 然后 下一个页面的容器(FrameLayout)添加此static的jzvd,其中在添加属性动画,播放不中断,没有违和感; 退出时同理;

//首先 Intent 跳转页面时 去除默认过渡效果;Intent intent = new Intent(ListViewToDetailActivity.this,DetailListViewActivity.class);intent.putExtra("attr", viewAttr);startActivity(intent);overridePendingTransition(0, 0);//onbindView中兼容详情返回列表的逻辑,用于添加详情页中去除的jzvd;@Overridepublic void onBindViewHolder(MyViewHolder holder, int position) {JzvdStdRv jzvdStdRv;//上次播放的jzvd(点击的item),需要parentview去除jzvd,防止添加出现异常;if (JzvdStdRv.CURRENT_JZVD != null && AutoPlayUtils.positionInList == position) {ViewParent parent = JzvdStdRv.CURRENT_JZVD.getParent();if (parent != null) {((ViewGroup) parent).removeView(JzvdStdRv.CURRENT_JZVD);}holder.container.removeAllViews();holder.container.addView(JzvdStdRv.CURRENT_JZVD, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));jzvdStdRv = (JzvdStdRv) JzvdStdRv.CURRENT_JZVD;} else {//如果其他的item,某个容器中没有jzvd(特殊情况下),则new一个jzvd;if (holder.container.getChildCount() == 0) {jzvdStdRv = new JzvdStdRv(holder.container.getContext());holder.container.addView(jzvdStdRv,new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));} else {jzvdStdRv = (JzvdStdRv) holder.container.getChildAt(0);}jzvdStdRv.setUp(Urls.videoUrls[0][position],Urls.videoTitles[0][position], Jzvd.SCREEN_NORMAL);Glide.with(holder.container.getContext()).load(Urls.videoPosters[0][position]).into(jzvdStdRv.posterImageView);}jzvdStdRv.setId(R.id.jzvdplayer);jzvdStdRv.setAtList(true);jzvdStdRv.setClickUi(new JzvdStdRv.ClickUi() {@Overridepublic void onClickUiToggle() {AutoPlayUtils.positionInList = position;jzvdStdRv.setAtList(false);//封装target大小,xy位置数据传递到下一个页面;ViewAttr attr = new ViewAttr();int[] location = new int[2];holder.container.getLocationInWindow(location);attr.setX(location[0]);attr.setY(location[1]);attr.setWidth(holder.container.getMeasuredWidth());attr.setHeight(holder.container.getMeasuredHeight());if (onVideoClick != null) onVideoClick.videoClick(holder.container, attr, position);jzvdStdRv.setClickUi(null);}@Overridepublic void onClickStart() {AutoPlayUtils.positionInList = position;}});}//设置第二个页面的 addOnPreDrawListener 事件,在绘制之前播放动画;container.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {@Overridepublic boolean onPreDraw() {container.getViewTreeObserver().removeOnPreDrawListener(this);//获取当前正在播放的jzvd,并取出来,填充到当前页面的容器vg中,继续播放;ViewParent parent = JzvdStdRv.CURRENT_JZVD.getParent();if (parent != null) {((ViewGroup) parent).removeView(JzvdStdRv.CURRENT_JZVD);}container.addView(JzvdStdRv.CURRENT_JZVD, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));currentAttr = new ViewAttr();int[] location = new int[2];container.getLocationInWindow(location);currentAttr.setX(location[0]);currentAttr.setY(location[1]);currentAttr.setWidth(container.getMeasuredWidth());currentAttr.setHeight(container.getMeasuredHeight());//属性动画工具类;new ViewMoveHelper(container, attr, currentAttr, DURATION).startAnim();AlphaAnimation animation = new AlphaAnimation(0, 1);animation.setDuration(DURATION);llContent.setAnimation(animation);animation.start();return true;}});//返回时,先开始动画移至原位,刷新上个页面同样去除过渡动画,通过adapter判断添加当前播放的jzvd;private void backAnimation() {new ViewMoveHelper(container, currentAttr, attr, DURATION).startAnim();llContent.setVisibility(View.GONE);container.postDelayed(new Runnable() {@Overridepublic void run() {ListViewToDetailActivity.listViewToDetailActivity.animateFinish();finish();overridePendingTransition(0, 0);}}, DURATION);}

列表中自动播放 AutoPlayUtils

现象: 根据滑动多少距离判断停止播放, 当滑动停止后,自动播放第一个可见的item;

public static int positionInList = -1;//记录当前播放列表位置/**onScrolled 中调用* @param firstVisiblePosition 首个可见item位置* @param lastVisiblePosition  最后一个可见item位置* @param percent              当item被遮挡percent/1时释放,percent取值0-1*/public static void onScrollReleaseAllVideos(int firstVisiblePosition, int lastVisiblePosition, float percent) {if (Jzvd.CURRENT_JZVD == null) return;if (positionInList >= 0) {//可见范围内,if ((positionInList <= firstVisiblePosition || positionInList >= lastVisiblePosition - 1)) {if (getViewVisiblePercent(Jzvd.CURRENT_JZVD) < percent) {Jzvd.releaseAllVideos();}}}}/*** @param view* @return 当前视图可见比列*/public static float getViewVisiblePercent(View view) {if (view == null) {return 0f;}float height = view.getHeight();Rect rect = new Rect();if (!view.getLocalVisibleRect(rect)) {return 0f;}//获取当前播放view可见高度; 返回可见高度和控件高度的比值;float visibleHeight = rect.bottom - rect.top;return visibleHeight / height;}/**newState == RecyclerView.SCROLL_STATE_IDLE 当滑动停止时,判断当前需要播放的item;* @param firstVisiblePosition 首个可见item位置* @param lastVisiblePosition  最后一个可见item位置*/public static void onScrollPlayVideo(RecyclerView recyclerView, int jzvdId, int firstVisiblePosition, int lastVisiblePosition) {if (JZUtils.isWifiConnected(recyclerView.getContext())) {for (int i = 0; i <= lastVisiblePosition - firstVisiblePosition; i++) {//在recyclerview中可见item中;View child = recyclerView.getChildAt(i);View view = child.findViewById(jzvdId);if (view != null && view instanceof Jzvd) {Jzvd player = (Jzvd) view;//如果可见item中, jzvd是全部可见;if (getViewVisiblePercent(player) == 1f) {//如果不是上一个在播放的jzvd,则开始播放,不然会出现滑动停止暂停播放切换;if (positionInList != i + firstVisiblePosition) {player.startButton.performClick();}break;}}}}}

Jzvd播放界面 主实现类

JzvdStd 具体播放界面

具体的播放界面,ui界面扩展功能; Jzvd的控制音量,亮度,快进,快退的基础dialog也于此处实现;

列出几个有特色的实现类:

  • JzvdStdTinyWindow 可自定义小窗播放
public void gotoScreenTiny() {Log.i(TAG, "startWindowTiny " + " [" + this.hashCode() + "] ");if (state == STATE_NORMAL || state == STATE_ERROR || state == STATE_AUTO_COMPLETE)return;//获取当前jzvd的parentview,移出当前jzvd,添加一个clonejzvd占位,防止出错,引用当前parentview;ViewGroup vg = (ViewGroup) getParent();vg.removeView(this);cloneAJzvd(vg);CONTAINER_LIST.add(vg);//获取当前decorview,添加刚移出的jzvd;ViewGroup vgg = (ViewGroup) (JZUtils.scanForActivity(getContext())).getWindow().getDecorView();//和他也没有关系FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(400, 400);lp.gravity = Gravity.RIGHT | Gravity.BOTTOM;//添加滑动事件等vgg.addView(this, lp);//进行一些显示隐藏的设置;setScreenTiny();}
  • JzvdStdTikTok 仿抖音

  • JzvdStdGetGif 支持生成gif

  • JzvdStdLockScreen 支持播放时锁功能

  • JzvdStdSpeed 变速播放

  • AGVideo 仿爱奇艺等播放样式,选集,倍速播放;

  • JzvdStdMp3 本质上就是播放的时候不隐藏缩略图


评估

首先这个三方android视频播放库的完成度已经非常高了,同时支持切换播放引擎,横竖屏切换,支持recyclerview,webview,传感器横屏,电量监听,保存gif等等功能;还有仿抖音和爱奇艺样式的demo, 非常值得学习;

说下改进点,其实是完善点吧[逃]

  • JzvdJzvdStd部分逻辑视频播放抽象部分和实现部分的抽取更加清晰就更完美了;
  • 某些功能类如果能单独拆分就更好了;
  • 某些变量也单独抽取;

android 视频播放器框架 [饺子播放器](https://github.com/Jzvd/JiaoZiVideoPlayer) 源码解析与评估相关推荐

  1. android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估

    文章目录 android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估 引言 使用方 ...

  2. Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析

    对类的植入锁定进行判断 几个可以对覆盖率跟踪的Java类定义进行instrument的API public byte[] instrument(final ClassReader reader) {f ...

  3. 饺子播放器解决视频黑边

    //黑边问题,视频两边的黑边难以去除,我直接用饺子播放器,饺子播放器能够解决黑边. //第一步 导入依赖: implementation 'com.github.bumptech.glide:glid ...

  4. [源码解析] 深度学习分布式训练框架 horovod (11) --- on spark --- GLOO 方案

    [源码解析] 深度学习分布式训练框架 horovod (11) - on spark - GLOO 方案 文章目录 [源码解析] 深度学习分布式训练框架 horovod (11) --- on spa ...

  5. android+jiaozi播放器,android视频播放-饺子播放器

    饺子播放器是android主流播放器中之一,使用简单,功能全面. 不要按照github文档来,api有变动. 1.依赖 这个地方也要添加glide的依赖,因为视频默认图需要glide加载. imple ...

  6. android视频播放-饺子播放器

    饺子播放器是android主流播放器中之一,使用简单,功能全面. 不要按照github文档来,api有变动. 常见问题 ERROR: Manifest merger failed : uses-sdk ...

  7. android第三方播放器--饺子播放器的使用

    前言 很多APP都会有视频播放的需求,博主现在做的也不例外.博主刚开始是自己用VideoView实现的播放器,bug很多,修复起来很费时间,于是在网上寻找好用的第三方播放器,在github上找到了饺子 ...

  8. 方便的视频播放器-饺子播放器

    转载地址:https://github.com/lipangit/JiaoZiVideoPlayer 参考视频:http://ke.atguigu.com/course/149/learn#lesso ...

  9. Android 饺子播放器插件使用

    最新一段时间在学习关于Android的视频开发相关知识,先研究了android 视频播放的原生开发:videoview.surfaceHolder+MediaPlayer,然后是第三方插件的使用,饺子 ...

  10. android饺子播放器屏幕全屏,记录一次 学习饺子播放器切换全屏播放原理

    学习的第一步就是先找到函数的入口:根据R.id.fullscreen这个全屏按钮的点击事件,我们很快就能定位到名叫gotoScreenFullscreen的函数. code line 737 publ ...

最新文章

  1. linux代码中能出现中文吗_Linux命令很熟悉,你知道它们的英文全称和中文解释吗?...
  2. PyQt 编程基本思想-HelloWorld
  3. 单机redis 主从实例
  4. php 解析配置文件,php 解析ini配置文件
  5. static、final、abstract基本作用
  6. zookeeper教程
  7. 【kali】kali环境下安装dvwa
  8. 第43课 最大公约数 《小学生C++趣味编程》
  9. sqlite和java_Java和SQLite
  10. ActiveMQ 在java中的使用,通过单例模式、工厂实现
  11. 借助 Resharper 和 StyleCop 让代码更整洁
  12. OSX malware and exploit collection (~100 files) + links and resources for OSX malware analysis
  13. FPGA三段式状态机的思维陷阱
  14. WPS通过宏工具批量修改图片尺寸
  15. ★用辩证数学解答“缸中之脑”
  16. Redis Operator学习笔记
  17. 【LEETCODE】【鱼缸难题】
  18. DNS污染,配置DNS
  19. Android通过来电号码识别姓名。
  20. Jekyll+GitHub搭建个人博客

热门文章

  1. 零基础怎么自学日语?
  2. mac 安装 JDK
  3. STM32芯片命名规则及含义
  4. 宋宝华:Linux文件读写(BIO)波澜壮阔的一生
  5. 微信小程序订阅消息 微信公众号模板消息
  6. 易语言c语言哪个做游戏脚本,游戏简易脚本制作教程
  7. 关于stata软件的一些问题
  8. 单总线led驱动芯片WS2811在linux下的驱动
  9. saas商业级的小程序商城(已开源)
  10. html菜鸟教程 很多代码,HTML5 代码编写规范简介