Android 使用ExoPlayer视频播放 (二)

匿名 (未验证) 提交于 2019-12-03 00:27:02

创建缓存文件夹

public class CachesUtil {       public static String VIDEO = "video";      /**      * 获取媒体缓存文件      *      * @param child      * @return      */     public static File getMediaCacheFile(String child) {         String directoryPath = "";         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {             // 外部储存可用             directoryPath = MyApplication.getContext().getExternalFilesDir(child).getAbsolutePath();         } else {             directoryPath = MyApplication.getContext().getFilesDir().getAbsolutePath() + File.separator + child;         }         File file = new File(directoryPath);         //判断文件目录是否存在         if (!file.exists()) {             file.mkdirs();         }         LogUtil.d(TAG, "getMediaCacheFile ====> " + directoryPath);         return file;     } }

创建带缓存的数据解析工厂

// 测量播放带宽,如果不需要可以传null TransferListener<? super DataSource> listener = new DefaultBandwidthMeter(); DefaultDataSourceFactory upstreamFactory = new DefaultDataSourceFactory(this, listener, new DefaultHttpDataSourceFactory("MyApplication", listener)); // 获取缓存文件夹 File file = CachesUtil.getMediaCacheFile(CachesUtil.VIDEO); Cache cache = new SimpleCache(file, new NoOpCacheEvictor()); // CacheDataSinkFactory 第二个参数为单个缓存文件大小,如果需要缓存的文件大小超过此限制,则会分片缓存,不影响播放 DataSink.Factory cacheWriteDataSinkFactory = new CacheDataSinkFactory(cache, Long.MAX_VALUE); CacheDataSourceFactory dataSourceFactory = new CacheDataSourceFactory(cache, upstreamFactory, new FileDataSourceFactory(), cacheWriteDataSinkFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null);

使用带缓存的数据解析工厂创建资源,和入门的使用一致

 Uri uri = Uri.parse(url); ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true);

添加AndroidVideoCache依赖

dependencies {     implementation'com.danikula:videocache:2.7.0' }

自定义缓存文件命名规则

public class CacheFileNameGenerator implements FileNameGenerator {      private static final String TAG = "CacheFileNameGenerator";      /**      * @param url      * @return      */     @Override     public String generate(String url) {         Uri uri = Uri.parse(url);         List<String> pathSegList = uri.getPathSegments();         String path = null;         if (pathSegList != null && pathSegList.size() > 0) {             path = pathSegList.get(pathSegList.size() - 1);         } else {             path = url;         }         Log.d(TAG, "generate return " + path);         return path;     } }

创建单例的AndroidVideoCache实例的方法

public class HttpProxyCacheUtil {      private static HttpProxyCacheServer videoProxy;      public static HttpProxyCacheServer getVideoProxy() {         if (videoProxy == null) {             videoProxy = new HttpProxyCacheServer.Builder(MyApplication.getContext())                     .cacheDirectory(CachesUtil.getMediaCacheFile(CachesUtil.VIDEO))                     .maxCacheSize(1024 * 1024 * 1024) // 缓存大小                     .fileNameGenerator(new CacheFileNameGenerator())                     .build();         }         return videoProxy;     } }

使用AndroidVideoCache进行缓存

HttpProxyCacheServer proxy = HttpProxyCacheUtil.getVideoProxy(); // 将url传入,AndroidVideoCache判断是否使用缓存文件 url = proxy.getProxyUrl(url); // 创建资源,准备播放 Uri uri = Uri.parse(url); ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true);
  • 自定义PlaybackControlView播放控制界面
    新建一个XML布局文件exo_playback_control_view,在这个布局文件里面设计我们想要的布局样式,在SimpleExoPlayerView控件中添加一个:
    app:controller_layout_id=”布局id”
    属性。来表明该SimpleExoPlayerView所对应的PlaybackControlView的布局。

这里要注意几个问题:

控件的id不能随便起,这些id都是定义好的,要与exoPlayer原来PlaybackControlView的布局控件id,名称一致,可通过源码查看具体有哪些id。现在给出部分id如下:

<item name="exo_play" type="id"/><!--播放--> <item name="exo_pause " type="id"/><!--暂停--> <item name="exo_rew " type="id"/><!--后退--> <item name="exo_ffwd" type="id"/><!--前进--> <item name="exo_prev" type="id"/><!--上一个--> <item name="exo_next" type="id"/><!--下一个--> <item name="exo_repeat_toggle " type="id"/><!--重复模式开关--> <item name="exo_duration " type="id"/><!--视频总时长--> <item name="exo_position " type="id"/><!--当前播放位置--> <item name="exo_progress  " type="id"/><!--播放总时长-->

布局的控件数量可以少(比如上一个,下一个这个功能我不想要,就可以不写,也就不会展示出来),但不能多,也不能出现没有定义的id。比如说:想在控制布局上添加一个展开全屏的按钮,那就实现不了
*DefaultTimeBar默认进度条
可以通过xml设置他的颜色,高度,大小等等

app:bar_height="2dp" app:buffered_color="#ffffff" app:played_color="#c15d3e" app:scrubber_color="#ffffff" app:scrubber_enabled_size="10dp" app:unplayed_color="#cdcdcd"

当我们需要添加更多按钮,比如全屏按钮时,初级自定义就没办法满足我们的需求,这是需要我们自定义重写SimpleExoPlayerView和PlaybackControlView这两个类。这里以添加全屏按钮为例。
* 自定义PlaybackControlView,添加全屏按钮,点击切换横屏
* 自定义SimpleExoPlayerView,使用自定义PlaybackControlView
* 切换横屏时隐藏其他布局,只显示视频控件,达到全屏效果
复制PlaybackControlView代码,新建ExoVideoPlayBackControlView为我们自定义视频控制类,复制SimpleExoPlayerView代码,新建ExoVideoPlayView为我们自定义视频播放控件,将其中使用的控制器换成ExoVideoPlayBackControlView。为ExoVideoPlayBackControlView新建XML文件view_exo_video_play_back_control,添加全屏按钮,再添加全屏播放时的标题栏布局和控制布局,具体界面按需求实现,并将他们隐藏,在全屏播放时在显示。这里全屏按钮的id不在默认定义的id列表中,所以使用”@+id/”自己定义

        <ImageButton             android:id="@+id/exo_fill"             android:layout_width="wrap_content"             android:layout_height="wrap_content"             android:layout_marginLeft="10dp"             android:layout_marginRight="10dp"             android:background="@null"             android:padding="5dp"             android:scaleType="centerInside"             android:src="@drawable/selector_video_fill" />

在构造方法中初始我们的布局和控件,给全屏按钮设置点击事件,点击时横屏,调整界面达成全屏的效果

public class ExoVideoPlayBackControlView extends FrameLayout {      static {         ExoPlayerLibraryInfo.registerModule("goog.exo.ui");     }       ...      private final ComponentListener componentListener;// 事件监听     private final View fillButton; //全屏按钮     private final View exoPlayerControllerBottom; // 默认控制器     private final View exoPlayerControllerTopLandscape; // 全屏标题     private final View exoPlayerControllerBottomLandscape; // 全屏控制器      ...      public ExoVideoPlayBackControlView(Context context, AttributeSet attrs, int defStyleAttr,AttributeSet playbackAttrs) {         super(context, attrs, defStyleAttr);         int controllerLayoutId = R.layout.view_exo_video_play_back_control;         componentListener = new ComponentListener();          ...           fillButton = findViewById(R.id.exo_fill);         if (fillButton != null) {             fillButton.setOnClickListener(componentListener);         }         exoPlayerControllerBottom = findViewById(R.id.exoPlayerControllerBottom);         exoPlayerControllerTopLandscape = findViewById(R.id.exoPlayerControllerTopLandscape);         exoPlayerControllerBottomLandscape = findViewById(R.id.exoPlayerControllerBottomLandscape);      }      ...      private final class ComponentListener extends Player.DefaultEventListener implements TimeBar.OnScrubListener, OnClickListener {      ...      @Override     public void onClick(View view) {         if (player != null) {             if (fillButton == view) {                 // 设置横屏                 changeOrientation(SENSOR_LANDSCAPE);             }         }      ...      } }

在ExoVideoPlayBackControlView切换横竖屏的方法中执行横竖屏切换回调,重新设置是否竖屏参数,修改状态栏属性,在显示和隐藏控制器视图的方法中也要修改状态栏属性

    private synchronized void changeOrientation(@OnOrientationChangedListener.SensorOrientationType int orientation) {          if (orientationListener == null) {             return;         }         // 执行回调         orientationListener.onOrientationChanged(orientation);          switch (orientation) {             case SENSOR_PORTRAIT:                 // 竖屏                 setPortrait(true);                 showSystemStatusUi();                 break;             case SENSOR_LANDSCAPE:                 // 横屏                 setPortrait(false);                 showSystemStatusUi();                 break;             case SENSOR_UNKNOWN:             default:                 break;         }     }          /**      * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will      * be automatically hidden after this duration of time has elapsed without user input.      */     public void show() {         if (!isVisible()) {             setVisibility(VISIBLE);             // 显示状态栏             showSystemStatusUi();             if (visibilityListener != null) {                 visibilityListener.onVisibilityChange(getVisibility());             }             updateAll();             requestPlayPauseFocus();         }         // Call hideAfterTimeout even if already visible to reset the timeout.         hideAfterTimeout();     }      /**      * Hides the controller.      */     public void hide() {         if (isVisible()) {             setVisibility(GONE);             if (visibilityListener != null) {                 visibilityListener.onVisibilityChange(getVisibility());             }             removeCallbacks(updateProgressAction);             removeCallbacks(hideAction);             hideAtMs = C.TIME_UNSET;             // 收起状态栏,全屏播放             hideSystemStatusUi();         }     }      public void setPortrait(boolean portrait) {         this.portrait = portrait;         // 根据横竖屏情况显示控制器视图         showControllerByDisplayMode();     }      /**      * 在切换横竖屏时和显示控制器视图显示状态栏      */     private void showSystemStatusUi() {         if (videoViewAccessor == null) {             return;         }         int flag = View.SYSTEM_UI_FLAG_VISIBLE;         videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);     }      /**      * 隐藏控制器视图时收起状态栏,全屏播放      */     private void hideSystemStatusUi() {         if (portrait) {             return;         }         if (videoViewAccessor == null) {             return;         }         WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);         if (windowManager == null) {             return;         }          int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE                 | View.SYSTEM_UI_FLAG_FULLSCREEN                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION                 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {             flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;         }          videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);     }      /**      * 横屏时设置横屏顶部标题和横屏底部控制器可见,竖屏时设置竖屏底部控制器可见      */     private void showControllerByDisplayMode() {         if (exoPlayerControllerTopLandscape != null) {             if (portrait) {                 exoPlayerControllerTopLandscape.setVisibility(INVISIBLE);             } else {                 exoPlayerControllerTopLandscape.setVisibility(VISIBLE);             }         }         if (exoPlayerControllerBottom != null) {             if (portrait) {                 exoPlayerControllerBottom.setVisibility(VISIBLE);             } else {                 exoPlayerControllerBottom.setVisibility(INVISIBLE);             }         }         if (exoPlayerControllerBottomLandscape != null) {             if (portrait) {                 exoPlayerControllerBottomLandscape.setVisibility(INVISIBLE);             } else {                 exoPlayerControllerBottomLandscape.setVisibility(VISIBLE);             }         }     }

自定义切换横竖屏监听,在activity中定义回调,并逐层传递activity -> ExoVideoPlayView -> ExoVideoPlayBackControlView,在回调中隐藏除了视频播放空间之外的控件,设置Window的flag,在隐藏显示状态栏时不改变原有布局

public interface OnOrientationChangedListener {     int SENSOR_UNKNOWN = -1;     int SENSOR_PORTRAIT = SENSOR_UNKNOWN + 1;     int SENSOR_LANDSCAPE = SENSOR_PORTRAIT + 1;      @IntDef({SENSOR_UNKNOWN, SENSOR_PORTRAIT, SENSOR_LANDSCAPE})     @Retention(RetentionPolicy.SOURCE)     @interface SensorOrientationType {      }      void onChanged(@SensorOrientationType int orientation); }
    evpvAlbumPlay.setOrientationListener(new ExoVideoPlayBackControlView.OrientationListener() {         @Override         public void onOrientationChanged(int orientation) {             if (orientation == SENSOR_PORTRAIT) {                 changeToPortrait();             } else if (orientation == SENSOR_LANDSCAPE) {                 changeToLandscape();             }          }     });      private void changeToPortrait() {         setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);         WindowManager.LayoutParams attr = getWindow().getAttributes();         Window window = getWindow();         window.setAttributes(attr);         window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);         rlTitle.setVisibility(View.VISIBLE);         llOthersAlbumPlay.setVisibility(View.VISIBLE);     }      private void changeToLandscape() {         setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);         WindowManager.LayoutParams lp = getWindow().getAttributes();         Window window = getWindow();         window.setAttributes(lp);         // 隐藏显示状态栏时不改变原有布局         window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);         rlTitle.setVisibility(View.GONE);         llOthersAlbumPlay.setVisibility(View.GONE);     }

重写ExoVideoPlayBackControlView的onKeyDown方法,在全屏模式下点击回退按钮,应切换回竖屏,竖屏时执行回退的回调

public class ExoVideoPlayBackControlView extends FrameLayout {      public interface ExoClickListener {          boolean onBackClick(@Nullable View view, boolean isPortrait);      }      @Override     public boolean onKeyDown(int keyCode, KeyEvent event) {         if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {             if (portrait) {                 if (exoClickListener != null) {                     exoClickListener.onBackClick(null, portrait);                 }             } else {                 changeOrientation(SENSOR_PORTRAIT);                 return true;             }         }         return super.onKeyDown(keyCode, event);     } }
    evpvAlbumPlay.setBackListener(new ExoVideoPlayBackControlView.ExoClickListener() {         @Override         public boolean onBackClick(@Nullable View view, boolean isPortrait) {             if (isPortrait) {                 finish();             }             return false;         }

至此,自定义ExoPlayer,点击全屏播放的功能基本完成,不过还有一些需要完善的地方,比如在全屏播放时显示控制器视图,上边的部分视图会被状态栏挡住,如果手机有虚拟导航栏,导航栏会遮住右边部分视图,所以还需要获取状态高度和虚拟导航栏高度,设置间距

        int navigationHeight = ScreenUtil.getNavigationHeight(context);         exoPlayerControllerBottom = findViewById(R.id.exoPlayerControllerBottom);         exoPlayerControllerTopLandscape = findViewById(R.id.exoPlayerControllerTopLandscape);         exoPlayerControllerTopLandscape.setPadding(0, ScreenUtil.getStatusHeight(context), navigationHeight, 0);         exoPlayerControllerBottomLandscape = findViewById(R.id.exoPlayerControllerBottomLandscape);         View llControllerBottomLandscape = findViewById(R.id.llControllerBottomLandscape);         llControllerBottomLandscape.setPadding(0, 0, navigationHeight, 0);         timeBarLandscape.setPadding(0, 0, navigationHeight, 0);
public class ScreenUtil { private ScreenUtil() {     private ScreenUtil() {         /* cannot be instantiated */         throw new UnsupportedOperationException("cannot be instantiated");     }      /**      * 获得状态栏的高度      *      * @param context      * @return      */     public static int getStatusHeight(Context context) {          int statusHeight = -1;         try {             Class<?> clazz = Class.forName("com.android.internal.R$dimen");             Object object = clazz.newInstance();             int height = Integer.parseInt(clazz.getField("status_bar_height")                     .get(object).toString());             statusHeight = context.getResources().getDimensionPixelSize(height);         } catch (Exception e) {             e.printStackTrace();         }         return statusHeight;     }      /**      * 获得NavigationHeight      *      * @param context      * @return      */     public static int getNavigationHeight(Context context) {         int navigationHeight = 0;         // 屏幕原始尺寸高度,包括虚拟功能键高度         int screenHeight = 0;         // 获取屏幕尺寸,不包括虚拟功能高度         int defaultDisplayHeight = 0;         WindowManager windowManager = (WindowManager) context                 .getSystemService(Context.WINDOW_SERVICE);         Display display = windowManager.getDefaultDisplay();         DisplayMetrics dm = new DisplayMetrics();         @SuppressWarnings("rawtypes")         Class c;         try {             c = Class.forName("android.view.Display");             @SuppressWarnings("unchecked")             Method method = c.getMethod("getRealMetrics", DisplayMetrics.class);             method.invoke(display, dm);             screenHeight = dm.heightPixels;         } catch (Exception e) {             e.printStackTrace();         }          Point outSize = new Point();         windowManager.getDefaultDisplay().getSize(outSize);         defaultDisplayHeight = outSize.y;          navigationHeight = screenHeight - defaultDisplayHeight;         return navigationHeight;     }  }

ExoPlayer的事件监听EventListener,通过Player的addListener方法和removeListener方法添加和删除。

public interface Player {    /**    * Listener of changes in player state.    */   interface EventListener {      /**      * Called when the timeline and/or manifest has been refreshed.      * <p>      * Note that if the timeline has changed then a position discontinuity may also have occurred.      * For example, the current period index may have changed as a result of periods being added or      * removed from the timeline. This will <em>not</em> be reported via a separate call to      * {@link #onPositionDiscontinuity(int)}.      *      * @param timeline The latest timeline. Never null, but may be empty.      * @param manifest The latest manifest. May be null.      */     void onTimelineChanged(Timeline timeline, Object manifest);      /**      * Called when the available or selected tracks change.      *      * @param trackGroups The available tracks. Never null, but may be of length zero.      * @param trackSelections The track selections for each renderer. Never null and always of      *     length {@link #getRendererCount()}, but may contain null elements.      */     void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);      /**      * Called when the player starts or stops loading the source.      *      * @param isLoading Whether the source is currently being loaded.      */     void onLoadingChanged(boolean isLoading);      /**      * Called when the value returned from either {@link #getPlayWhenReady()} or      * {@link #getPlaybackState()} changes.      *      * @param playWhenReady Whether playback will proceed when ready.      * @param playbackState One of the {@code STATE} constants.      */     void onPlayerStateChanged(boolean playWhenReady, int playbackState);      /**      * Called when the value of {@link #getRepeatMode()} changes.      *      * @param repeatMode The {@link RepeatMode} used for playback.      */     void onRepeatModeChanged(@RepeatMode int repeatMode);      /**      * Called when the value of {@link #getShuffleModeEnabled()} changes.      *      * @param shuffleModeEnabled Whether shuffling of windows is enabled.      */     void onShuffleModeEnabledChanged(boolean shuffleModeEnabled);      /**      * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}      * immediately after this method is called. The player instance can still be used, and      * {@link #release()} must still be called on the player should it no longer be required.      *      * @param error The error.      */     void onPlayerError(ExoPlaybackException error);      /**      * Called when a position discontinuity occurs without a change to the timeline. A position      * discontinuity occurs when the current window or period index changes (as a result of playback      * transitioning from one period in the timeline to the next), or when the playback position      * jumps within the period currently being played (as a result of a seek being performed, or      * when the source introduces a discontinuity internally).      * <p>      * When a position discontinuity occurs as a result of a change to the timeline this method is      * <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.      *      * @param reason The {@link DiscontinuityReason} responsible for the discontinuity.      */     void onPositionDiscontinuity(@DiscontinuityReason int reason);      /**      * Called when the current playback parameters change. The playback parameters may change due to      * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change      * them (for example, if audio playback switches to passthrough mode, where speed adjustment is      * no longer possible).      *      * @param playbackParameters The playback parameters.      */     void onPlaybackParametersChanged(PlaybackParameters playbackParameters);      /**      * Called when all pending seek requests have been processed by the player. This is guaranteed      * to happen after any necessary changes to the player state were reported to      * {@link #onPlayerStateChanged(boolean, int)}.      */     void onSeekProcessed();    } }

其中onPlayerStateChanged方法返回了是否正在播放和播放状态,播放状态一共以下几种:

public interface Player {   /**    * The player does not have any media to play.    */   int STATE_IDLE = 1;   /**    * The player is not able to immediately play from its current position. This state typically    * occurs when more data needs to be loaded.    */   int STATE_BUFFERING = 2;   /**    * The player is able to immediately play from its current position. The player will be playing if    * {@link #getPlayWhenReady()} is true, and paused otherwise.    */   int STATE_READY = 3;   /**    * The player has finished playing the media.    */   int STATE_ENDED = 4; }

具体使用可参考SimpleExoPlayerView和PlaybackControlView,这两个类中的ComponentListener类实现了这个事件监听。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!