The paper

Video playback is a common scenario in our development. In the past two years, the popularity of video has been increasing. It can be said that the previous two years were the years of live broadcasting, and this year is the year of small video, with a variety of short video applications. The business scenarios for video are becoming more and more rich, with more and more functions. For our development, the code of playing related components is becoming more and more complex, and the cost of management and maintenance is becoming higher and higher. Facing the business of constant iteration, we need an effective solution to deal with such frequent business changes.

In recent years, I have been engaged in video-related businesses, including adaptation development of mobile and TV terminals. MediaPlayer, Exoplayer, iJkPlayer, VLC, FFmpeg, etc. There were many problems along the way… To say more is tears, in order to adapt to the changing product needs, the middle of the reconstruction of N multiple versions. Eventually, PlayerBase was born. PlayerBase3 has been completely reconfigured, and the current framework is basically stable. It is easy to handle for most application video playback component scenarios.

^_^ star portal –> project address: github.com/jiajunhui/P…

QQ communication group: 600201778, there are problems in the group directly put forward, after seeing will answer one by one.

P graph technology is limited, the picture in the article will do!

Introduction of framework

Please pay attention! Please pay attention! Please pay attention! PlayerBase differs from most player packaging libraries.

PlayerBase is a solution framework for componentizing decoders and playback views. Whatever decoder you need to implement the abstraction of the framework definition can be introduced, and views, whether control views or business views within the player, can be componentized. Player development becomes clear and simple, more conducive to product iteration.

PlayerBase doesn’t make any extra functional components for you, unlike most PlayerBase libraries which configure or inherit and then rewrite and customize the functional components you need and shield the functional components you don’t need. . The right direction should be to add what components are needed and remove them when they are not, rather than to use them when they are already provided.

features

  • Componentization of views

  • High reuse and low coupling of view components

  • Componentization and configuration management of decoding scheme

  • Complete customization of view components

  • Hot-plug view components, added when used and removed when not in use

  • Custom access to various decoding schemes

  • Switch of decoding scheme

  • Supports double speed playback

  • Supports Window mode

  • Supports seamless continuation of Window mode

  • Supports seamless continuation of tabular mode

  • Supports seamless continuation across pages

  • Support to adjust the screen display ratio

  • Support dynamic adjustment of render view types

  • Support VideoView Angle cutting processing, edge shadow effect

  • Provide custom data providers

  • Unified event delivery mechanism

  • The addition of extended events

  • The function such as…

Partial Usage Examples

  1. Decode configuration and framework initialization
public class App extends Application {
    @Override
    public void onCreate(a) {
        / /...
        // If you want to use the default network state event producer, add this line configuration.
        / / and you need to add permissions android. Permission. ACCESS_NETWORK_STATE
        PlayerConfig.setUseDefaultNetworkEventProducer(true);
        // Set the default decoder
        int defaultPlanId = 1;
        PlayerConfig.addDecoderPlan(new DecoderPlan(defaultPlanId, IjkPlayer.class.getName(), "IjkPlayer"));
PlayerConfig.setDefaultPlanId(defaultPlanId);
        // Initialize the library
        PlayerLibrary.init(this); }}Copy the code
  1. Assemble components (add the components you need [components are user-defined, the framework does not provide any view components])
ReceiverGroup receiverGroup = new ReceiverGroup();
/ / Loading components
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
/ / Controller component
receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
/ / CompleteCover components
receiverGroup.addReceiver(KEY_COMPLETE_COVER, new CompleteCover(context));
/ / the Error component
receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
Copy the code
  1. Set the component to start playing
BaseVideoView videoView = findViewById(R.id.videoView);
videoView.setReceiverGroup(receiverGroup);
DataSource data = new DataSource("http://url...");
videoView.setDataSource(data);
videoView.start();
Copy the code
  1. Event monitoring
//player event
videoView.setOnPlayerEventListener(new OnPlayerEventListener(){
    @Override
    public void onPlayerEvent(int eventCode, Bundle bundle){
        / /...}});//receiver event
videoView.setOnReceiverEventListener(new OnReceiverEventListener(){
    @Override
    public void onReceiverEvent(int eventCode, Bundle bundle) {
        / /...}});Copy the code

See github project home page and Wiki introduction for detailed usage examples

Frame design

View processing

Don’t look down upon a small player, which is really a different world. Sometimes view components are so complex that you wonder about life.

Let’s start by looking at some common view scenarios for player development:




These are some of the most common views (there are many more, such as sharpness switches, video lists, completion prompts, etc.) that can spiral out of control without an effective way to manage them.

The controller view, load view, gesture view, error view, barrage view and advertisement view are listed above. All these views are closely linked to playback, driven by playback state, and may coexist and restrict each other.

So how can these views be managed uniformly? The layout file alone is enough to drink, even with include management still can’t get rid of the display level management problem. It would be horrible to write it all in one XML… The improved version generally wraps each component into a View and then writes it to the layout separately, which is obviously easier than the previous one. However, communication between players and components, and communication between components, is a problem. There are still problems:

  1. The hierarchy of the component layout is entirely determined by the layout file, which is the only way to change it. Not friendly.
  2. Components and players are completely bound, the coupling degree is quite high, player and components, components and components of the communication is completely direct use of reference to operate, if the product says that a component to be removed or greatly changed, you cry, change not good hands are likely to bring a pile of bugs. The components are highly coupled and cannot be pluggable. This is the biggest obstacle.
  3. Components are difficult to reuse.

Now, let’s see what PlayerBase does.

The concept of Receiver and Cover

As anyone who has done player development knows, all view work is driven by state events, which are the main thread. This could be an event from the player (such as a decoder error), an event from a view (such as a gesture to adjust the playback progress), or an external event (such as a network state change).

This information we can boil down to

  • Views are both event receivers and event producers
  • The decoder is the event producer
  • There may be external event producers

That is, we treat the view as an event receiver, and the view has the ability to send events.

The decoder constantly issues its own state of affairs to be passed to the view.

Some external events also need to be passed to the view

At this point, the framework defines the concept of the event receiver, which can produce events as well as being the event consumer, and the overlay inherits from the receiver introducing the View.

public abstract class BaseReceiver implements IReceiver {
    / /...
    protected final void notifyReceiverEvent(int eventCode, Bundle bundle){
        / /..
    }
    /** * all player event dispatch by this method. */
    void onPlayerEvent(int eventCode, Bundle bundle);

    /** * error event. */
    void onErrorEvent(int eventCode, Bundle bundle);
    /** * receivers event. */
    void onReceiverEvent(int eventCode, Bundle bundle);

    /**
     * you can call this method dispatch private event for a receiver.
     *
     * @return Bundle Return value after the receiver's response, nullable.
     */
    @Nullable
    Bundle onPrivateEvent(int eventCode, Bundle bundle);
    
}
Copy the code
public abstract class BaseCover extends BaseReceiver{
    / /...
    public abstract View onCreateCoverView(Context context);
    / /...
}
Copy the code

And look at the code, there are player events, there are error events, there are events between components (Receiver). How do I send these events? If I have N recipients, how do I break them?

Recipient Group Management (ReceiverGroup)

ReceiverGroup comes with the purpose of unified management, unified event distribution, and of course, data sharing. To picture:

ReceiverGroup includes Cover (also known as Receiver) and Receiver, and provides operations such as adding, removing, traversing, and destroying receivers. When events need to be delivered, they can be delivered through ReceiverGroup in a unified manner.

public interface IReceiverGroup {
    void setOnReceiverGroupChangeListener(OnReceiverGroupChangeListener onReceiverGroupChangeListener);
    /**
     * add a receiver, you need put a unique key for this receiver.
     * @param key
     * @param receiver
     */
    void addReceiver(String key, IReceiver receiver);
    /**
     * remove a receiver by key.
     * @param key
     */
    void removeReceiver(String key);
    /**
     * loop all receivers
     * @param onLoopListener
     */
    void forEach(OnLoopListener onLoopListener);
    /**
     * loop all receivers by a receiver filter.
     * @param filter
     * @param onLoopListener
     */
    void forEach(OnReceiverFilter filter, OnLoopListener onLoopListener);
    /**
     * get receiver by key.
     * @param key
     * @param <T>
     * @return* /
    <T extends IReceiver> T getReceiver(String key);
    /**
     * get the ReceiverGroup group value.
     * @return* /
    GroupValue getGroupValue(a);
    /** * clean receivers. */
    void clearReceivers(a);
}
Copy the code

Data sharing between components (GroupValue)

In player development, we often need to restrict the function or state of another view according to the state of one view. For example, it is forbidden to drag the progress bar when loading, or prohibit other view operations after error is displayed. These are all state constraints.

GroupValue provides a shared data pool. When a certain data is refreshed, the callback interface listening to the data can be notified in time and can directly obtain the data status. You can specify which update events you want to listen for, and if you register the key value of the data you want to listen for, you will receive a callback when the corresponding value is updated. You can then control the UI view in the callback.

public class CustomCover extends BaseCover{
    / /...
    @Override
    public void onReceiverBind(a) {
        super.onReceiverBind();
        getGroupValue().registerOnGroupValueUpdateListener(mOnGroupValueUpdateListener);
    }
    / /...
    private IReceiverGroup.OnGroupValueUpdateListener mOnGroupValueUpdateListener =
            new IReceiverGroup.OnGroupValueUpdateListener() {
        @Override
        public String[] filterKeys() {
            return new String[]{ DataInter.Key.KEY_COMPLETE_SHOW };
        }
        @Override
        public void onValueUpdate(String key, Object value) {
            / /...}};/ /...
    @Override
    public void onReceiverUnBind(a) {
        super.onReceiverUnBind(); getGroupValue().unregisterOnGroupValueUpdateListener(mOnGroupValueUpdateListener); }}Copy the code

Layout management of views (hierarchical populating)

With the common view components mentioned above, we are bound to encounter override priority issues. For example, after the Error view appears, no other view is visible, that is to say, the Error view has the highest priority and no one can block it. We create one Cover view after another, and the placement of the view needs a view’s CoverLevel to control. Cover views at different levels are placed in containers at different levels.

The summary is as follows:

  • Specify the priority CoverLevel of the Cover
  • Cover components are automatically placed separately based on the Level value when they are added

Schematic diagramCode sample

public class CustomCover extends BaseCover{
    / /...
    @Override
    public int getCoverLevel(a) {
        return ICover.COVER_LEVEL_LOW;
    }
    / /...
}
Copy the code

Default view container manager

public class DefaultLevelCoverContainer extends BaseLevelCoverContainer {
    / /...
    @Override
    protected void onAvailableCoverAdd(BaseCover cover) {
        super.onAvailableCoverAdd(cover);
        switch (cover.getCoverLevel()){
            case ICover.COVER_LEVEL_LOW:
                mLevelLowCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
                break;
            case ICover.COVER_LEVEL_MEDIUM:
                mLevelMediumCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
                break;
            case ICover.COVER_LEVEL_HIGH:
                mLevelHighCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
                break; }}/ /...
}
Copy the code

Component hierarchy

As shown in figure:

EventProducer

As the name implies, it is the source of events. For example, when the network status of the system changes, a notification is sent, and each application adjusts the display or Settings according to its own situation. Or changes in battery power and low battery warning notification events.

For another example, we need to display the data of the barrage view above. The barrage data comes from the server, and we need to continuously obtain data from the server and display it in the barrage view. The process of retrieving data and sending it to the view can be regarded as an event producer continuously producing the event of data update of the barrage, and constantly sending the event to the view to refresh the display during data update of the barrage.

An example of a network change event producer comes with the framework:

public class NetworkEventProducer extends BaseEventProducer {
    / /...
    private Handler mHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case MSG_CODE_NETWORK_CHANGE:
                    int state = (int) msg.obj;
                    / /... Send the network status
                    getSender().sendInt(InterKey.KEY_NETWORK_STATE, state);
                    PLog.d(TAG,"onNetworkChange : " + state);
                    break; }}};/ /...
    public NetworkEventProducer(Context context){
        / /...
    }
    / /...
    public static class NetChangeBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            / /...
            //post state message
        }
        / /...}}Copy the code

Since events issued by the event producer are for the Receiver, they are called back to onReceiverEvent(). If key-value data is sent, they are placed in GroupValue. The following code:

public class CustomCover extends BaseCover{
    / /...
    @Override
    public void onReceiverEvent(int eventCode, Bundle bundle) {
        / /...
    }
    / /...
}
Copy the code

DataProvider (DataProvider)

DataProvider is designed for the unification of broadcast control and elegance of use.

During development, we may encounter scenarios where the data source you get is just an ID, not a uri or URL that can be played directly, and you need to use this ID to request an interface to get the source address of the play. We usually request the interface first and then set the player to play the source data after a successful callback.

DataProviderIs designed to isolate this process and wrap it up as a data provider (or data producer) that takes the data and sends it out. And all you have to do is give that ID toDataProviderThat’s it. The rest of the processDataProviderTo finish.DataProviderThe specific implementation of the user needs to be done.

public class MonitorDataProvider extends BaseDataProvider {
    / /...
    public MonitorDataProvider(a){
        / /...
    }
    private Handler mHandler = new Handler(Looper.getMainLooper());
    @Override
    public void handleSourceData(DataSource sourceData) {
        this.mDataSource = sourceData;
        / /... provider start
        onProviderDataStart();
        / /...
        / /... Call the data back out
        onProviderMediaDataSuccess(bundle);
        / /...
        / /... When abnormal
        onProviderError(-1.null)}/ /...
    @Override
    public void cancel(a) {
        / /... cancel something
    }
    @Override
    public void destroy(a) {
        / /... destroy something}}Copy the code

Note: The data provider must be set before starting playback.

Function using

The use of the VideoView

It can be roughly summarized as the following steps:

  1. Initialize The VideoView and set up the corresponding listening event or data provider
  2. Use ReceiverGroup to assemble the required components, Cover and Receiver
  3. Set up the component to VideoView
  4. Set the data to play
  5. Pause operations such as resuming playback
  6. Destroy player
public class VideoViewActivity extends AppCompatActivity implements OnPlayerEventListener{
    / /...
    BaseVideoView mVideoView;
    @Override
    public void onCreate(Bundle saveInstance){
        super.onCreate(saveInstance);
        mVideoView = findViewById(R.id.videoView);
        mVideoView.setOnPlayerEventListener(this);
        // Set the data provider MonitorDataProvider
        MonitorDataProvider dataProvider = new MonitorDataProvider();
        mVideoView.setDataProvider(dataProvider);
        / /...
        ReceiverGroup receiverGroup = new ReceiverGroup();
        / / Loading components
        receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
        / / Controller component
        receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
        / / CompleteCover components
        receiverGroup.addReceiver(KEY_COMPLETE_COVER, new CompleteCover(context));
        / / the Error component
        receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
        / /...
        DataSource data = new DataSource("monitor_id");
        videoView.setDataSource(data);
        videoView.start();
    }
    / /...
    public void onPlayerEvent(int eventCode, Bundle bundle){
        switch (eventCode){
            case OnPlayerEventListener.PLAYER_EVENT_ON_VIDEO_RENDER_START:
                / /...
                break;
            case OnPlayerEventListener.PLAYER_EVENT_ON_PLAY_COMPLETE:
                / /...
                break; }}/ /...
    @Override
    public void onPause(a){
        super.onPause();
        mVideoView.pause();
        / /...
    }
    @Override
    public void onResume(a){
        super.onResume();
        mVideoView.onResume();
        / /...
    }
    @Override
    public void onDestroy(a){
        super.onDestroy();
        mVideoView.stopPlayback();
        / /...}}Copy the code

The use of AVPlayer

If you want to directly use AVPlayer to process the playback yourself, the general steps are as follows:

  1. Initialize an AVPlayer object.
  2. Initialize a SuperContainer object and set ReceiverGroup into SuperContainer.
  3. Set up a Render view with SuperContainer, then handle the RenderCallBack and associate the decoder yourself.
SuperContainer mSuperContainer = new SuperContainer(context);
ReceiverGroup receiverGroup = new ReceiverGroup();
/ /... add some covers
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
mSuperContainer.setReceiverGroup(receiverGroup);
/ /...
final RenderTextureView render = new RenderTextureView(mAppContext);
render.setTakeOverSurfaceTexture(true);
//....
mPlayer.setOnPlayerEventListener(new OnPlayerEventListener() {
    @Override
    public void onPlayerEvent(int eventCode, Bundle bundle) {
        / /... There are some specific Settings that need to be self-implemented depending on the event
        / /... For example, the size of the video needs to pass the Render refresh measurement or the Angle of the video, etc
        // Distribute events to subviewsmSuperContainer.dispatchPlayEvent(eventCode, bundle); }}); mPlayer.setOnErrorEventListener(new OnErrorEventListener() {
    @Override
    public void onErrorEvent(int eventCode, Bundle bundle) {
        // Distribute events to subviewsmSuperContainer.dispatchErrorEvent(eventCode, bundle); }});/ /...
render.setRenderCallback(new IRender.IRenderCallback() {
    @Override
    public void onSurfaceCreated(IRender.IRenderHolder renderHolder, int width, int height) {
        mRenderHolder = renderHolder;
        bindRenderHolder(mRenderHolder);
    }
    @Override
    public void onSurfaceChanged(IRender.IRenderHolder renderHolder, int format, int width, int height) {}@Override
    public void onSurfaceDestroy(IRender.IRenderHolder renderHolder) {
        mRenderHolder = null; }}); mSuperContainer.setRenderView(render.getRenderView()); mPlayer.setDataSource(dataSource); mPlayer.start();Copy the code

If you don’t have to, try to use the BaseVideoView wrapped by the framework for playback. The framework is relatively complete and provides rich callback and customization.

Use of RelationAssist

Today’s short video apps all have scenarios like this:

  • Play in a list
  • The list jumps and details are played naturally without pausing

For the first one to play in a list, VideoView could theoretically do it, but VideoView is too heavy for a list. A lightweight treatment scheme is required.

But for the second, VideoView is not good, VideoView is the decoder is wrapped, when jump to the next page, is a new page naturally have a new view, can not use the previous page player instance to render the current page play.

In fact, for this seamless continuation, the principle is very simple. Use the same decoding instance for different render views. It can be simply likened to a MediaPlayer that constantly sets up different surface renders and plays. If you want to handle this process yourself, you need to deal with the Render callbacks and associated to the decoder, and you need to deal with Render measurements and display scale, Angle, etc.

RelationAssist is designed to simplify this process. When switching between pages or views, you simply supply and pass in the view container (of type ViewGroup) for the location. Internal complex setup items and correlation are done by RelationAssist.

public class TestActivity extends AppcompatActivity{
    / /...
    RelationAssist mAssist;
    ViewGroup view2;
    public void onCreate(Bundle saveInstance){
    	super.onCreate(saveInstance);
    	/ /...
    	mAssist = new RelationAssist(this);
    	mAssist.setEventAssistHandler(eventHandler);
    	mReceiverGroup = ReceiverGroupManager.get().getLiteReceiverGroup(this);
    	mAssist.setReceiverGroup(mReceiverGroup);
    	DataSource dataSource = new DataSource();
    	dataSource.setData("http://...");
    	dataSource.setTitle("xxx");
    	mAssist.setDataSource(dataSource);
    	mAssist.attachContainer(mVideoContainer);
    	mAssist.play();
    	/ /...
    	switchPlay(view2);
    }
    / /...
    private void switchPlay(ViewGroup container){ mAssist.attachContainer(container); }}Copy the code

If you want to correlate across pages, just wrap RelationAssist as a singleton yourself. The code is not shown here. For detailed code, please refer to github project demo code.

Event Helper Handler

Basic operations in the view, such as pause, replay, retry, resume, and so on, are ultimately passed to the decoder for relevant operations. There may also be user-defined events such as playing next or last.

For basic operational events (pause, resume, replay, and so on), this can be done automatically within the framework, while user-defined events need to be handled by the user. EventAssistHandler is connected to both BaseVideoView and RelationAssist within the framework. When you use it, you need to pass in an available event handler object, which can be processed according to different event parameters. The following code:

mVideoView.setOnVideoViewEventHandler(new OnVideoViewEventHandler(){
    @Override
        public void onAssistHandle(BaseVideoView assist, int eventCode, Bundle bundle) {
            // The basic event handling is already done in the superclass super. If you need to override it, just override the corresponding method.
            super.onAssistHandle(assist, eventCode, bundle);
            switch (eventCode){
                case DataInter.Event.EVENT_CODE_REQUEST_NEXT:
                    / /... Play the next one
                    break; }}});Copy the code

Play in Window mode

We may sometimes need to play in small Windows so as not to interrupt the user’s browsing. The frame is specifically designed for the use of Window play. The framework provides two types of Window-related components.

  • WindowVideoView
  • FloatWindow

WindowVideoView is almost the same as VideoView, except that WindowVideoView is presented as a Window. Windows can be dragged by default. If you don’t need it, you can disable it. Each of the Settings for Windows has a default value.

FloatWindowParams windowParams = new FloatWindowParams();
windowParams.setWindowType(WindowManager.LayoutParams.TYPE_TOAST)
            .setFormat(PixelFormat.RGBA_8888)
            .setFlag(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
            .setDefaultAnimation(true)
            .setX(100)
            .setY(100)
            .setWidth(width)
            .setHeight(height)
            .setGravity(Gravity.TOP | Gravity.LEFT));
mWindowVideoView = new WindowVideoView(this,windowParams);
/ /...
Copy the code

Whereas FloatWindow is just a hover window View, you can pass in the layout View that you want to display. Can be used for the seamless continuation of the window switch play. No code examples are shown here.

Some StyleSetter Settings

Styles are set for VideoView, WindowVideoView, and FloatWindow. Of course the framework provides stylesetters that you can use elsewhere. The following style Settings are provided:

public interface IStyleSetter {
    // Set rounded corners
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    void setRoundRectShape(float radius);
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    void setRoundRectShape(Rect rect, float radius);
    // Set it to circle
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    void setOvalRectShape(a);
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    void setOvalRectShape(Rect rect);
    // Clear style Settings
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    void clearShapeStyle(a);
    // Set the shadow
    // Note that the shadow setting requires that the corresponding View object must have a background color (not TRANSPARENT)
    // If you do not set it, the frame will be set to black
    void setElevationShadow(float elevation);
    void setElevationShadow(int backgroundColor, float elevation);
}
Copy the code

Access to decoder

The framework has its own decoding implementation of the system MediaPlayer. Ijkplayer and Exoplayer are included in the sample of the project demo. If you want to access other decoders, please refer to the sample code.

Access to the steps

  1. Inherited from BaseInternalPlayer
  2. Implement defined abstract methods
  3. Configuration import your decoder
public class XXXPlayer extends BaseInternalPlayer{
    public XXXPlayer(a) {
        / /...
    }
    / /...
    //implements some abstract methods.
}
Copy the code

Use the decoder through configuration Settings.

int planId = 2;
PlayerConfig.addDecoderPlan(new DecoderPlan(planId, XXXPlayer.class.getName(), "XXXPlayer"));
PlayerConfig.setDefaultPlanId(planId);
Copy the code

The PlayerBase tutorial is almost complete. Code word so tired!!

That’s about it. See the project source code for more details.

If you have any questions, contact: [email protected]

QQ communication group: 600201778

Finally, please attach the project address: PlayerBase