06. Player UI extraction and encapsulation

catalogue

  • 01. Video player UI packaging requirements
  • 02. Player UI architecture diagram
  • 03. How to separate playback and UI separation
  • 04. How to implement VideoPlayer
  • 05. VideoController implementation
  • 06. Play Player and UI communication
  • 07. How to add a custom playback view
  • 08. About the player View hierarchy
  • 09. Video player gravity sensor monitoring

00. General framework for video player

  • Basic package video player, can be in ExoPlayer, MediaPlayer, audio network RTC video player kernel, native MediaPlayer can be freely toggle
  • For view state switching and later maintenance expansion, avoid coupling between functions and services. For example, you need to support player UI customization rather than the UI code in the lib library
  • For video playback, audio playback, playback, and live video functions. Simple to use, strong code expansion, good encapsulation, mainly and business completely decoupled, exposed interface monitoring for developers to deal with specific business logic
  • The overall architecture of the player: player kernel (free switching) + video player + play while caching + highly customized PLAYER UI view layer
  • Project address: github.com/yangchong21…
  • About the overall function of the video player: juejin.cn/post/688345…

01. Video player UI packaging requirements

  • Problems encountered in development
    • The player can be played in multiple scenarios. Multiple products may use the same player, which causes a problem. When the player status of one player service changes, other player services must update the player status synchronously. Development and maintenance costs can increase dramatically, making subsequent development unsustainable.
  • The player kernel is coupled to the UI layer
    • In other words, the video player and UI are softer together, especially the interaction between the two. For example, the UI progress bar needs to be updated during playback, and the abnormal UI needs to be displayed during playback. It is difficult to handle the UI update operation when the player status changes
  • The UI is difficult to customize or modify
    • Common video players, for example, write the various views of the video to XML, which can be very large in the later code, and changing a small layout can have a big impact. In this way, in the later stage, the code is often only added, but not removed…
    • Sometimes it is difficult to adapt to a new scene, such as adding a play AD, a teacher to start a class, or a video to guide the business needs, and you need to write a lot of business code into the player. In the late iteration, the open and closed principle is violated, and the video player needs to be separated from the business
  • The video player structure needs to be clear
    • This refers to whether the video player can quickly get started after reading the document and know the general process of packaging. It is convenient for others to modify and maintain later, so the video player function needs to be separated. For example, switch kernel + video player (Player + Controller + View)
  • Be sure to decouple
    • Decoupling of player player and video UI: Supports adding custom video views, such as adding custom ads, novice guidance, or video playback exception views, which requires strong scalability
  • Suitable for various service scenarios
    • For example, it is suitable for playing single videos, multiple videos, and list videos, or one video per page like Douyin, and there are small Windows for playing videos. That is, for most business scenarios

02. Player UI architecture diagram

03. How to separate playback and UI separation

  • This helps the playback service change
    • The change of playback status causes cross-synchronization between different playback service scenarios. The direct control of playback services on the player is removed and interface listening is used to decouple the player. For example: player + controller + interface
  • About Video Player
    • Define a video player InterVideoPlayer interface, operation video play, pause, buffering, progress Settings, set play mode and other operations.
    • Then write a concrete implementation class for the player interface, get the kernel player in this class, and do the relevant implementation operations.
  • About the video View View
    • Define a view InterVideoController interface, mainly responsible for view show/hide, play progress, lock screen, status bar and other operations.
    • You then write a concrete implementation class for the player view interface, where the inflate view is operated and the interface method is implemented. To make it easier for developers to customize the view later, you need the addView action, which assembles the added view into a Map collection.
  • The player interacts with the Controller
    • When you create a BaseVideoController object in the Player, you need to add the Controller to the player, and there are two important things you need to do to pass the player state listener and the playmode listener to the controller
    • SetPlayState Sets the playback logic state of the video player, mainly including buffering, loading, playing, pause, error, completion, abnormality, playing progress and other states, so as to facilitate the controller to do UI update operations
    • SetPlayerState Sets the state of video playback switching mode, which is mainly normal mode, small window mode and normal mode, which is convenient for the controller to do UI update
  • The player interacts with the View
    • This is very critical, for example, video playback failure needs to display the control layer of the exception View View; To initialize video playing, display loading and update the UI playback progress bar. It’s all player and view layer interaction
    • You can define a class that implements both the InterVideoPlayer interface and the InterVideoController interface, and then re-implement all the methods of both interfaces. The purpose of this class is to call both the VideoPlayer API and BaseVideoController API from the InterControlView interface implementation class
  • How to add a custom player view
    • Added custom player view, such as adding video ads, can choose to skip, choose to pause play. So this view, this view, definitely needs to manipulate the player or get the state of the player. In this case, it is necessary to expose the status interface listening for the video playback
    • First define an InterControlView interface, that is to say, all custom video view view needs to realize this interface, the core method of the interface is: binding view to the player, view display hidden change monitoring, play state monitoring, play mode monitoring, progress monitoring, lock screen monitoring and so on
    • In the BaseVideoController state listener, the player state can be passed to subclasses through the InterControlView interface object

04. How to implement VideoPlayer

  • The code is shown below. Some code is omitted. See demo for details
    public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout
            implements InterVideoPlayer.VideoPlayerListener {
    
        private Context mContext;
        /** * player */
        protected P mMediaPlayer;
        /** * instantiate the playback core */
        protected PlayerFactory<P> mPlayerFactory;
        /** * Controller */
        @Nullable
        protected BaseVideoController mVideoController;
        /** * the container that actually hosts the player view */
        protected FrameLayout mPlayerContainer;
    
        public VideoPlayer(@NonNull Context context) {
            this(context, null);
        }
    
        public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mContext = context;
            init(attrs);
        }
    
        private void init(AttributeSet attrs) {
            BaseToast.init(mContext.getApplicationContext());
            // Read the global configuration
            initConfig();
            // Read the configuration in XML and synthesize the global configuration
            initAttrs(attrs);
            initView();
        }
    
        private void initConfig(a) {
            VideoPlayerConfig config = VideoViewManager.getConfig();
            mEnableAudioFocus = config.mEnableAudioFocus;
            mProgressManager = config.mProgressManager;
            mPlayerFactory = config.mPlayerFactory;
            mCurrentScreenScaleType = config.mScreenScaleType;
            mRenderViewFactory = config.mRenderViewFactory;
            // Sets whether to print logs
            VideoLogUtils.setIsLog(config.mIsEnableLog);
        }
    
        @Override
        protected Parcelable onSaveInstanceState(a) {
            VideoLogUtils.d("onSaveInstanceState: " + mCurrentPosition);
            // The activity may be recycled by the system after it is cut to the background, so save the progress here
            saveProgress();
            return super.onSaveInstanceState();
        }
    
        private void initAttrs(AttributeSet attrs) {
            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.VideoPlayer);
            mEnableAudioFocus = a.getBoolean(R.styleable.VideoPlayer_enableAudioFocus, mEnableAudioFocus);
            mIsLooping = a.getBoolean(R.styleable.VideoPlayer_looping, false);
            mCurrentScreenScaleType = a.getInt(R.styleable.VideoPlayer_screenScaleType, mCurrentScreenScaleType);
            mPlayerBackgroundColor = a.getColor(R.styleable.VideoPlayer_playerBackgroundColor, Color.BLACK);
            a.recycle();
        }
    
        /** * Initialize the player view */
        protected void initView(a) {
            mPlayerContainer = new FrameLayout(getContext());
            // Set the background color, currently set to black
            mPlayerContainer.setBackgroundColor(mPlayerBackgroundColor);
            LayoutParams params = new LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT);
            // Add the layout to the view
            this.addView(mPlayerContainer, params);
        }
    
        /** * to set the controller, null to remove the controller *@param mediaController                           controller
         */
        public void setController(@Nullable BaseVideoController mediaController) {
            mPlayerContainer.removeView(mVideoController);
            mVideoController = mediaController;
            if(mediaController ! =null) {
                mediaController.setMediaPlayer(this);
                LayoutParams params = newLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mPlayerContainer.addView(mVideoController, params); }}/** * start playing, note: this method must be called after {@link#release()} release the player, otherwise it will leak memory */
        @Override
        public void start(a) {
            if (mVideoController==null) {Before calling the start method, initialize the video controller and call the setController method
                throw new VideoException(VideoException.CODE_NOT_SET_CONTROLLER,
                        "Controller must not be null , please setController first");
            }
            boolean isStarted = false;
            if (isInIdleState() || isInStartAbortState()) {
                isStarted = startPlay();
            } else if (isInPlaybackState()) {
                startInPlaybackState();
                isStarted = true;
            }
            if (isStarted) {
                mPlayerContainer.setKeepScreenOn(true);
                if(mAudioFocusHelper ! =null){ mAudioFocusHelper.requestFocus(); }}}/** * first play *@returnWhether the playback starts successfully */
        protected boolean startPlay(a) {
            // Do not continue playing if mobile network prompt is displayed
            if (showNetWarning()) {
                // Stop playing
                setPlayState(ConstantKeys.CurrentState.STATE_START_ABORT);
                return false;
            }
            // Listen for audio focus changes
            if (mEnableAudioFocus) {
                mAudioFocusHelper = new AudioFocusHelper(this);
            }
            // Read the playback progress
            if(mProgressManager ! =null) {
                mCurrentPosition = mProgressManager.getSavedProgress(mUrl);
            }
            initPlayer();
            addDisplay();
            startPrepare(false);
            return true;
        }
    
    
        /** * Initializes the player */
        protected void initPlayer(a) {
            // Create objects in factory mode
            mMediaPlayer = mPlayerFactory.createPlayer(mContext);
            mMediaPlayer.setPlayerEventListener(this);
            setInitOptions();
            mMediaPlayer.initPlayer();
            setOptions();
        }
    
        /** * Whether to display mobile network prompt, can be configured in Controller */
        protected boolean showNetWarning(a) {
            // Play the local data source without detecting the network
            if (VideoPlayerHelper.instance().isLocalDataSource(mUrl,mAssetFileDescriptor)){
                return false;
            }
            returnmVideoController ! =null && mVideoController.showNetWarning();
        }
    
    
        /** * The configuration item before initialization */
        protected void setInitOptions(a) {}/** * Configuration item */ after initialization
        protected void setOptions(a) {
            // Set whether to loop
            mMediaPlayer.setLooping(mIsLooping);
        }
    
        /** * Initialize the video render View */
        protected void addDisplay(a) {
            if(mRenderView ! =null) {
                mPlayerContainer.removeView(mRenderView.getView());
                mRenderView.release();
            }
            // Create a TextureView object
            mRenderView = mRenderViewFactory.createRenderView(mContext);
            // Bind the mMediaPlayer object
            mRenderView.attachToPlayer(mMediaPlayer);
            LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
            mPlayerContainer.addView(mRenderView.getView(), 0, params);
        }
    
        /** * Start ready to play (direct play) */
        protected void startPrepare(boolean reset) {
            if (reset) {
                mMediaPlayer.reset();
                // Resets option. After media player reset, option will become invalid
                setOptions();
            }
            if(prepareDataSource()) { mMediaPlayer.prepareAsync(); setPlayState(ConstantKeys.CurrentState.STATE_PREPARING); setPlayerState(isFullScreen() ? ConstantKeys.PlayMode.MODE_FULL_SCREEN : isTinyScreen() ? ConstantKeys.PlayMode.MODE_TINY_WINDOW : ConstantKeys.PlayMode.MODE_NORMAL); }}/** * set playback data *@returnWhether playing data is set successfully */
        protected boolean prepareDataSource(a) {
            if(mAssetFileDescriptor ! =null) {
                mMediaPlayer.setDataSource(mAssetFileDescriptor);
                return true;
            } else if(! TextUtils.isEmpty(mUrl)) { mMediaPlayer.setDataSource(mUrl, mHeaders);return true;
            }
            return false;
        }
    
        /** * Video playback error callback */
        @Override
        public void onError(a) {
            mPlayerContainer.setKeepScreenOn(false);
            setPlayState(ConstantKeys.CurrentState.STATE_ERROR);
            VideoPlayerConfig config = VideoViewManager.getConfig();
            if(config! =null&& config.mBuriedPointEvent! =null) {// Enter the video page
                if (PlayerUtils.isConnected(mContext)){
                    config.mBuriedPointEvent.onError(mUrl,false);
                } else {
                    config.mBuriedPointEvent.onError(mUrl,true); }}}/** * The video playback is complete callback */
        @Override
        public void onCompletion(a) {
            mPlayerContainer.setKeepScreenOn(false);
            mCurrentPosition = 0;
            if(mProgressManager ! =null) {
                // Clear the progress
                mProgressManager.saveProgress(mUrl, 0);
            }
            setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING);
            VideoPlayerConfig config = VideoViewManager.getConfig();
            if(config! =null&& config.mBuriedPointEvent! =null) {// The video has finished playingconfig.mBuriedPointEvent.playerCompletion(mUrl); }}@Override
        public void onInfo(int what, int extra) {
            switch (what) {
                case PlayerConstant.MEDIA_INFO_BUFFERING_START:
                    setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PAUSED);
                    break;
                case PlayerConstant.MEDIA_INFO_BUFFERING_END:
                    setPlayState(ConstantKeys.CurrentState.STATE_COMPLETED);
                    break;
                case PlayerConstant.MEDIA_INFO_VIDEO_RENDERING_START: // The video starts rendering
                    setPlayState(ConstantKeys.CurrentState.STATE_PLAYING);
                    if(mPlayerContainer.getWindowVisibility() ! = VISIBLE) { pause(); }break;
                case PlayerConstant.MEDIA_INFO_VIDEO_ROTATION_CHANGED:
                    if(mRenderView ! =null)
                        mRenderView.setVideoRotation(extra);
                    break; }}/** * Callback */ when the video is buffered and ready to play
        @Override
        public void onPrepared(a) {
            setPlayState(ConstantKeys.CurrentState.STATE_PREPARED);
            if (mCurrentPosition > 0) { seekTo(mCurrentPosition); }}/** * Gets the current player status */
        public int getCurrentPlayerState(a) {
            return mCurrentPlayerState;
        }
    
    
        /** * Set the playback state to the Controller, which is used to control the UI display of the Controller. It mainly refers to the various states of the player * -1 playing error * 0 playing not started * 1 playing ready * 2 Playing ready * 3 playing * 4 pause playing * 5 buffering (when the player is playing, the buffer data is insufficient, buffering, Buffer data enough to resume playing) * 6 pause buffering (when the player is playing, the buffer data is insufficient, buffer, pause the player, continue buffering, buffer data enough resume pause * 7 play finished * 8 start play stop */
        protected void setPlayState(@ConstantKeys.CurrentStateType int playState) {
            mCurrentPlayState = playState;
            if(mVideoController ! =null) {
                mVideoController.setPlayState(playState);
            }
            if(mOnStateChangeListeners ! =null) {
                for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
                    if(l ! =null) { l.onPlayStateChanged(playState); }}}}/** * Set player state to Controller, including full screen state and non-full screen state * Play mode * Normal mode, small window mode, Normal mode One of the three modes * MODE_NORMAL Normal mode * MODE_FULL_SCREEN full screen mode * MODE_TINY_WINDOW Small screen mode */
        protected void setPlayerState(@ConstantKeys.PlayModeType int playerState) {
            mCurrentPlayerState = playerState;
            if(mVideoController ! =null) {
                mVideoController.setPlayerState(playerState);
            }
            if(mOnStateChangeListeners ! =null) {
                for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
                    if(l ! =null) { l.onPlayerStateChanged(playerState); }}}}/** * Empty implementation of OnStateChangeListener. Just override the required method */
        public static class SimpleOnStateChangeListener implements OnVideoStateListener {
            @Override
            public void onPlayerStateChanged(@ConstantKeys.PlayModeType int playerState) {}
            @Override
            public void onPlayStateChanged(int playState) {}}/** * Add a playback status listener that will be called when the playback status changes. * /
        public void addOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if (mOnStateChangeListeners == null) {
                mOnStateChangeListeners = new ArrayList<>();
            }
            mOnStateChangeListeners.add(listener);
        }
    
        /** * remove a playback status listener */
        public void removeOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if(mOnStateChangeListeners ! =null) { mOnStateChangeListeners.remove(listener); }}/** * If you want to set multiple listeners at the same time, it is recommended {@link# addOnStateChangeListener (OnVideoStateListener)}. * /
        public void setOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if (mOnStateChangeListeners == null) {
                mOnStateChangeListeners = new ArrayList<>();
            } else {
                mOnStateChangeListeners.clear();
            }
            mOnStateChangeListeners.add(listener);
        }
    
        /** * remove all playback status listeners */
        public void clearOnStateChangeListeners(a) {
            if(mOnStateChangeListeners ! =null) { mOnStateChangeListeners.clear(); }}/** * changes the return key logic for the activity */
        public boolean onBackPressed(a) {
            returnmVideoController ! =null && mVideoController.onBackPressed();
        }
    
    
        / * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- exposed API method -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * * /
        / * * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- exposed API method -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * * /
    
    
        public void setVideoBuilder(VideoPlayerBuilder videoBuilder){
            if (mPlayerContainer==null || videoBuilder==null) {return;
            }
            // Set the background color of the video player
            mPlayerContainer.setBackgroundColor(videoBuilder.mColor);
            // Set the width and height of the small screen
            if(videoBuilder.mTinyScreenSize! =null && videoBuilder.mTinyScreenSize.length>0){
                mTinyScreenSize = videoBuilder.mTinyScreenSize;
            }
            // Seek to the preset position as soon as it starts playing
            if (videoBuilder.mCurrentPosition>0) {this.mCurrentPosition = videoBuilder.mCurrentPosition;
            }
            // Whether to enable AudioFocus listening. This function is enabled by default
            this.mEnableAudioFocus = videoBuilder.mEnableAudioFocus; }}Copy the code

05. VideoController implementation

  • The code is shown below, the code is too long, part of the code is omitted, see the demo for details
    public abstract class BaseVideoController extends FrameLayout implements InterVideoController.OrientationHelper.OnOrientationChangeListener {
    
        // Player wrapper class, which combines MediaPlayerControl API and IVideoController API
        protected ControlWrapper mControlWrapper;
    
        public BaseVideoController(@NonNull Context context) {
            / / create
            this(context, null);
        }
    
        public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs) {
            / / create
            this(context, attrs, 0);
        }
    
        public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView(context);
        }
    
        @Override
        protected void onDetachedFromWindow(a) {
            super.onDetachedFromWindow();
            if(mShowAnim ! =null){
                mShowAnim.cancel();
                mShowAnim = null;
            }
            if(mHideAnim ! =null){
                mHideAnim.cancel();
                mHideAnim = null; }}@Override
        public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
            if (mControlWrapper.isPlaying() && (mEnableOrientation || mControlWrapper.isFullScreen())) {
                if (hasWindowFocus) {
                    postDelayed(new Runnable() {
                        @Override
                        public void run(a) { mOrientationHelper.enable(); }},800);
                } else{ mOrientationHelper.disable(); }}}protected void initView(Context context) {
            if(getLayoutId() ! =0) {
                LayoutInflater.from(getContext()).inflate(getLayoutId(), this.true);
            }
            mOrientationHelper = new OrientationHelper(context.getApplicationContext());
            mEnableOrientation = VideoViewManager.getConfig().mEnableOrientation;
            mAdaptCutout = VideoViewManager.getConfig().mAdaptCutout;
            mShowAnim = new AlphaAnimation(0f.1f);
            mShowAnim.setDuration(300);
            mHideAnim = new AlphaAnimation(1f.0f);
            mHideAnim.setDuration(300);
            mActivity = PlayerUtils.scanForActivity(context);
        }
    
        /** * Sets the controller layout file. Subclasses must implement */
        protected abstract int getLayoutId(a);
    
        /** * important: this method is used to add {@linkVideoPlayer} is bound to the controller */
        @CallSuper
        public void setMediaPlayer(InterVideoPlayer mediaPlayer) {
            mControlWrapper = new ControlWrapper(mediaPlayer, this);
            // Bind the ControlComponent and Controller
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.attach(mControlWrapper);
            }
            // Start monitoring device direction
            mOrientationHelper.setOnOrientationChangeListener(this);
        }
    
        /** * add the ControlComponent at the bottom. Organize the order of addition so that the ControlComponent is placed at different levels */
        public void addControlComponent(InterControlView... component) {
            for (InterControlView item : component) {
                addControlComponent(item, false); }}/** * add the ControlComponent at the bottom. Organize the order of addition so that the ControlComponent is placed at different levels **@paramIs isPrivate a unique component? If so, it is not added to the controller */
        public void addControlComponent(InterControlView component, boolean isPrivate) {
            mControlComponents.put(component, isPrivate);
            if(mControlWrapper ! =null) {
                component.attach(mControlWrapper);
            }
            View view = component.getView();
            if(view ! =null && !isPrivate) {
                addView(view, 0); }}/** * Remove the control component */
        public void removeControlComponent(InterControlView component) {
            removeView(component.getView());
            mControlComponents.remove(component);
        }
    
        /** * Remove all components */
        public void removeAllControlComponent(a) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                removeView(next.getKey().getView());
            }
            mControlComponents.clear();
        }
    
        public void removeAllPrivateComponents(a) {
            Iterator<Map.Entry<InterControlView, Boolean>> it = mControlComponents.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<InterControlView, Boolean> next = it.next();
                if(next.getValue()) { it.remove(); }}}/ * * * {@linkVideoPlayer} calls this method to set the play state to the controller. * use the annotation qualifier here, do not use 1,2 as an intuitive number, it is not convenient to know the meaning * play state, It mainly refers to the various states of the player * -1 playing error * 0 playing not started * 1 playing ready * 2 Playing ready * 3 playing * 4 pause playing * 5 buffering (when the player is playing, the buffer data is insufficient, buffering, Buffer data enough to resume playing) * 6 pause buffering (when the player is playing, the buffer data is insufficient, buffer, pause the player, continue buffering, buffer data enough resume pause * 7 play finished * 8 start play stop */
        @CallSuper
        public void setPlayState(@ConstantKeys.CurrentStateType int playState) {
            // Set the player state
            handlePlayStateChanged(playState);
        }
    
        / * * * {@linkVideoPlayer} calls this method to set the player state * play mode * Normal mode, small window mode, Normal mode One of the three modes * MODE_NORMAL Normal mode * MODE_FULL_SCREEN full screen mode * MODE_TINY_WINDOW Small screen mode */
        @CallSuper
        public void setPlayerState(@ConstantKeys.PlayModeType final int playerState) {
            // Call this method to set the player state to the controller
            handlePlayerStateChanged(playerState);
        }
    
        /** * Play and pause */
        protected void togglePlay(a) {
            mControlWrapper.togglePlay();
        }
    
        /** * Switch between vertical and horizontal */
        protected void toggleFullScreen(a) {
            if(PlayerUtils.isActivityLiving(mActivity)){ mControlWrapper.toggleFullScreen(mActivity); }}/** * subclass please use this method to enter full screen **@returnCheck whether full screen */ is displayed successfully
        protected boolean startFullScreen(a) {
            if(! PlayerUtils.isActivityLiving(mActivity)) {return false;
            }
            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            mControlWrapper.startFullScreen();
            return true;
        }
    
        /** * subclass use this method to exit full screen **@returnWhether to exit full screen successfully */
        protected boolean stopFullScreen(a) {
            if(! PlayerUtils.isActivityLiving(mActivity)) {return false;
            }
            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            mControlWrapper.stopFullScreen();
            return true;
        }
    
        /** * changes the return key logic for the activity */
        public boolean onBackPressed(a) {
            return false;
        }
    
        /** * Whether to rotate automatically, default does not rotate automatically */
        public void setEnableOrientation(boolean enableOrientation) {
            mEnableOrientation = enableOrientation;
        }
    
        private void handleVisibilityChanged(boolean isVisible, Animation anim) {
            if(! mIsLocked) {// Deliver this event to the ControlComponent only if it is not locked
                for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                    InterControlView component = next.getKey();
                    component.onVisibilityChanged(isVisible, anim);
                }
            }
            onVisibilityChanged(isVisible, anim);
        }
    
        /** * subclasses override this method to listen for control to show and hide **@paramIsVisible whether visible *@paramAnim Show/hide animation */
        protected void onVisibilityChanged(boolean isVisible, Animation anim) {}private void handlePlayStateChanged(int playState) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.onPlayStateChanged(playState);
            }
            onPlayStateChanged(playState);
        }
    
        /** * subclasses override this method and update the controller UI */ in different playback states
        @CallSuper
        protected void onPlayStateChanged(int playState) {
            switch (playState) {
                case ConstantKeys.CurrentState.STATE_IDLE:
                    mOrientationHelper.disable();
                    mOrientation = 0;
                    mIsLocked = false;
                    mShowing = false;
                    removeAllPrivateComponents();
                    break;
                case ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING:
                    mIsLocked = false;
                    mShowing = false;
                    break;
                case ConstantKeys.CurrentState.STATE_ERROR:
                    mShowing = false;
                    break; }}/** * Player status changes *@paramPlayerState Player status */
        private void handlePlayerStateChanged(int playerState) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.onPlayerStateChanged(playerState);
            }
            onPlayerStateChanged(playerState);
        }
    
        /** * subclasses override this method and update the controller UI in different player states * normal mode, small window mode, Normal mode One of the three modes * MODE_NORMAL Normal mode * MODE_FULL_SCREEN full screen mode * MODE_TINY_WINDOW Small screen mode */
        @CallSuper
        protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) {
            switch (playerState) {
                case ConstantKeys.PlayMode.MODE_NORMAL:
                    // If the video is playing normally, the listener is set
                    if (mEnableOrientation) {
                        // Check whether automatic rotation is enabled
                        mOrientationHelper.enable();
                    } else {
                        // Cancel the listener
                        mOrientationHelper.disable();
                    }
                    if (hasCutout()) {
                        StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), false);
                    }
                    break;
                case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                    // Enforce device orientation in full screen mode
                    mOrientationHelper.enable();
                    if (hasCutout()) {
                        StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), true);
                    }
                    break;
                case ConstantKeys.PlayMode.MODE_TINY_WINDOW:
                    // Small window to cancel gravity sensor listening
                    mOrientationHelper.disable();
                    break; }}private void handleSetProgress(int duration, int position) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.setProgress(duration, position);
            }
            setProgress(duration, position);
        }
    
        /** * Refresh progress callback, where subclasses can listen for progress updates and then update UI **@paramDuration Total video duration *@paramPosition Current video duration */
        protected void setProgress(int duration, int position) {}private void handleLockStateChanged(boolean isLocked) {
            for(Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.onLockStateChanged(isLocked); } onLockStateChanged(isLocked); }}Copy the code

06. Play Player and UI communication

  • For example, in a custom view, if I want to call the VideoPlayer API, I can call the BaseVideoController API, how do I do that?
    • When you create the following object, you can get the API methods in both Player and Controller, but you can skip some of the code in the demo
    public class ControlWrapper implements InterVideoPlayer.InterVideoController {
        
        private InterVideoPlayer mVideoPlayer;
        private InterVideoController mController;
        
        public ControlWrapper(@NonNull InterVideoPlayer videoPlayer, @NonNull InterVideoController controller) {
            mVideoPlayer = videoPlayer;
            mController = controller;
        }
        
        @Override
        public void start(a) {
            mVideoPlayer.start();
        }
    
        @Override
        public void pause(a) {
            mVideoPlayer.pause();
        }
    
        @Override
        public long getDuration(a) {
            return mVideoPlayer.getDuration();
        }
    
        @Override
        public boolean isShowing(a) {
            return mController.isShowing();
        }
    
        @Override
        public void setLocked(boolean locked) { mController.setLocked(locked); }}Copy the code

07. How to add a custom playback view

  • For example, there is a business requirement to add an AD view at the beginning of the video player, wait 120 seconds for the AD countdown, and then go straight to the video logic. I believe this business scenario is very common, you have encountered, using the player is particularly simple, the code is as follows:
  • I’m going to create a custom view, and I’m going to implement the InterControlView interface, and I’m going to rewrite all the abstract methods in that interface, and I’m going to omit a lot of code here, see the demo.
    public class AdControlView extends FrameLayout implements InterControlView.View.OnClickListener {
    
        private ControlWrapper mControlWrapper;
        public AdControlView(@NonNull Context context) {
            super(context);
            init(context);
        }
    
        private void init(Context context){
            LayoutInflater.from(getContext()).inflate(R.layout.layout_ad_control_view, this.true);
        }
       
        /** ** Play status * -1 play error * 0 Play not started * 1 Play ready * 2 Play ready * 3 Playing * 4 Pause playing * 5 Buffering (when the player is playing, the buffer is not enough, buffer, Buffer data enough to resume playing) * 6 pause buffering (when the player is playing, the buffer data is insufficient, buffering, pause the player, continue buffering, buffer data enough to resume pause * 7 play finished * 8 start play stop *@paramPlayState refers to the state of the player */
        @Override
        public void onPlayStateChanged(int playState) {
            switch (playState) {
                case ConstantKeys.CurrentState.STATE_PLAYING:
                    mControlWrapper.startProgress();
                    mPlayButton.setSelected(true);
                    break;
                case ConstantKeys.CurrentState.STATE_PAUSED:
                    mPlayButton.setSelected(false);
                    break; }}/** * Play mode * Normal mode, small window mode, or normal mode * MODE_NORMAL Normal mode * MODE_FULL_SCREEN full screen mode * MODE_TINY_WINDOW Small screen mode *@paramPlayerState Play mode */
        @Override
        public void onPlayerStateChanged(int playerState) {
            switch (playerState) {
                case ConstantKeys.PlayMode.MODE_NORMAL:
                    mBack.setVisibility(GONE);
                    mFullScreen.setSelected(false);
                    break;
                case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                    mBack.setVisibility(VISIBLE);
                    mFullScreen.setSelected(true);
                    break;
            }
            // The full screen adaptation logic is not implemented yet, you need to complete}}Copy the code
  • And then how do I use this custom view? Very simple, in the previous basis, through the controller object add, code as shown below
    controller = new BasisVideoController(this);
    AdControlView adControlView = new AdControlView(this);
    adControlView.setListener(new AdControlView.AdControlListener() {
        @Override
        public void onAdClick(a) {
            BaseToast.showRoundRectToast( "AD click jump");
        }
    
        @Override
        public void onSkipAd(a) { playVideo(); }}); controller.addControlComponent(adControlView);// Set the controller
    mVideoPlayer.setController(controller);
    mVideoPlayer.setUrl(proxyUrl);
    mVideoPlayer.start();
    Copy the code

08. About the player View hierarchy

  • In order to expand the video player, it is necessary to expose the View interface for external developers to customize the video player view and add it to the controller of the player in the form of addView.
    • This involves the hierarchy of views. It is especially important to control the display and hiding of views, and to get the player state in the custom view
  • Take a simple example, basic video player
    • Added several playback views with basic playback capabilities. There are play completion, play exception, play load, top title bar, bottom control bar, lock screen, and gesture slider. How do you control their show and hide toggle?
    • In addView, most views are hidden by default GONE. For example, when the video is initialized, buffering first shows the buffered view and hides other views, and then playing shows the top/bottom view and hides other views
  • For example, what if I need to display two different custom views
    • For example, if you click on the video while it’s playing, it will show the top title view and the bottom control bar view, so it will show both views.
    • Click the back button of the top title view to close the player, and click the play pause button of the bottom control bar view to control the playing conditions. At this point, the ChildView of the bottom control bar view FrameLayout is at the bottom of the whole video, and the ChildView of the top title view FrameLayout is at the top of the whole video, so that the upper and lower levels can be corresponding events.
  • So FrameLayout layer upon layer, how do you make the lower layer not respond to events
    • Add: Android :clickable=”true” to the top layer to avoid clicking on the top layer to trigger the bottom layer. Alternatively, you can set the control to a background color.
  • For example, the view hierarchy of the base player looks like this
    // Add auto-complete playback interface view
    CustomCompleteView completeView = new CustomCompleteView(mContext);
    completeView.setVisibility(GONE);
    this.addControlComponent(completeView);
    
    // Add error view
    CustomErrorView errorView = new CustomErrorView(mContext);
    errorView.setVisibility(GONE);
    this.addControlComponent(errorView);
    
    // Add and load view interface view, ready to play interface
    CustomPrepareView prepareView = new CustomPrepareView(mContext);
    thumb = prepareView.getThumb();
    prepareView.setClickStart();
    this.addControlComponent(prepareView);
    
    // Add a title bar
    titleView = new CustomTitleView(mContext);
    titleView.setTitle(title);
    titleView.setVisibility(VISIBLE);
    this.addControlComponent(titleView);
    
    if (isLive) {
        // Add the bottom playback control bar
        CustomLiveControlView liveControlView = new CustomLiveControlView(mContext);
        this.addControlComponent(liveControlView);
    } else {
        // Add the bottom playback control bar
        CustomBottomView vodControlView = new CustomBottomView(mContext);
        // Whether to display the bottom progress bar. The default display
        vodControlView.showBottomProgress(true);
        this.addControlComponent(vodControlView);
    }
    // Add a sliding control view
    CustomGestureView gestureControlView = new CustomGestureView(mContext);
    this.addControlComponent(gestureControlView);
    Copy the code

09. Video player gravity sensor monitoring

  • Distinguish between different video playback modes
    • During normal playback, check whether automatic rotation is enabled and listen is enabled
    • When a video is played in full-screen mode, the device direction is forced to be monitored
    • Disable gravity sensing when playing video in small window mode
    • Be careful. There is a method setting switch that can be exposed to external developers as to whether to turn on auto-rotating gravity sensor listening. Let the user choose whether to enable this function
  • First write a class, then inherit the Class OrientationEventListener, note that the video player gravity sensing listener is not so frequent. Indicates that the test is performed once in 500 milliseconds……
    public class OrientationHelper extends OrientationEventListener {
    
        private long mLastTime;
    
        private OnOrientationChangeListener mOnOrientationChangeListener;
    
        public OrientationHelper(Context context) {
            super(context);
        }
    
        @Override
        public void onOrientationChanged(int orientation) {
            long currentTime = System.currentTimeMillis();
            if (currentTime - mLastTime < 500) {
                return;
            }
            // Check once in 500 milliseconds
            if(mOnOrientationChangeListener ! =null) {
                mOnOrientationChangeListener.onOrientationChanged(orientation);
            }
            mLastTime = currentTime;
        }
    
    
        public interface OnOrientationChangeListener {
            void onOrientationChanged(int orientation);
        }
    
        public void setOnOrientationChangeListener(OnOrientationChangeListener onOrientationChangeListener) { mOnOrientationChangeListener = onOrientationChangeListener; }}Copy the code
  • When the status of the player’s playing mode changes, it is necessary to update whether to turn on or turn off the gravity sensor listening. The code is shown below
    /** * subclasses override this method and update the controller UI in different player states * normal mode, small window mode, Normal mode One of the three modes * MODE_NORMAL Normal mode * MODE_FULL_SCREEN full screen mode * MODE_TINY_WINDOW Small screen mode */
    @CallSuper
    protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) {
        switch (playerState) {
            case ConstantKeys.PlayMode.MODE_NORMAL:
                // If the video is playing normally, the listener is set
                if (mEnableOrientation) {
                    // Check whether automatic rotation is enabled
                    mOrientationHelper.enable();
                } else {
                    // Cancel the listener
                    mOrientationHelper.disable();
                }
                break;
            case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                // Enforce device orientation in full screen mode
                mOrientationHelper.enable();
                break;
            case ConstantKeys.PlayMode.MODE_TINY_WINDOW:
                // Small window to cancel gravity sensor listening
                mOrientationHelper.disable();
                break; }}Copy the code