Strategy system

更新时间:
复制 MD 格式

This topic describes the design background, core concepts, built-in strategies, and usage of the AliPlayerKit Strategy System.

Background

The Strategy System was designed to address the conflict between business customization and framework evolution in large-scale AUI Kit projects.

As a low-code solution, AUI Kits must support UI and core process customization for different businesses. If business logic is embedded directly into the framework code, it creates high coupling. This can cause code conflicts, break business logic during framework upgrades, and hinder version iterations.

This type of logic is a business requirement, not a core framework feature. Without a proper mechanism to handle it, business-specific logic can creep into the framework. This threatens the system's stability and ability to evolve.

The Strategy System solves this problem:

  • It abstracts business-specific logic into independent, extensible strategy units.

  • It decouples business logic from the framework's core.

  • Businesses can extend the system at the strategy layer without modifying the framework.

This mechanism allows business capabilities to become a systematic and reusable foundation. It creates an iterative, reusable, and extensible base that supports the long-term evolution of AliPlayerKit.

Concepts

What is a strategy system?

The Strategy System is an architectural mechanism that unifies the management of strategy components. It is responsible for the strategy registration mechanism, lifecycle management, context injection, and event subscription. It also manages the creation, start, pause, reset, and destruction of strategies while the player is running.
Using this mechanism, developers can enable or disable strategies as needed, quickly implement custom policies, or completely replace default strategies to meet business customization needs.

What is a strategy?

A Strategy is an encapsulated unit of player business logic. It implements a specific business capability or custom logic, such as time-to-first-frame (TTFF) statistics, stuttering detection, traffic protection, or resume playback.
Core features:

  • Single responsibility: Focuses on an independent functional goal.

  • Event-driven: Responds to player status through the event bus.

  • Independent and isolated: Strategies do not interfere with each other.

Through a pluggable and composable design, non-core capabilities are abstracted into strategy units. This keeps the player core simple while providing flexible extension capabilities.

Features

Problems solved

The Strategy System addresses the following pain points:

  1. Difficult maintenance: Scattered business logic is hard to maintain and reuse.

  2. Lack of configuration: Different scenarios require different combinations of logic, but there is no flexible configuration mechanism.

  3. Source code modification: Custom logic requires modifying the framework's source code.

  4. Instance interference: In multi-player scenarios, business logic between instances can interfere with each other.

Core value

The Strategy System abstracts and encapsulates the player's monitoring and analysis logic. It allows customers to configure and combine strategies as needed.

Usage method

Description

Advantage

Use default strategies

The player component uses the official default strategies.

Simplifies the integration process. It works out-of-the-box.

Custom policy

Implement strategies for specific business requirements.

Meets various customized business needs.

Use no strategies

Use only the playback feature.

For pure playback scenarios with no logic dependencies.

Architectural advantages:

  • Decoupling: Strategies are separated from the player's core logic, creating clear responsibilities.

  • Reusability: Strategies can be reused across different player instances.

  • Extensibility: You can customize strategies without modifying the framework.

  • Isolation: Strategies are independent and do not interfere with one another.

Core capabilities

Capability

Description

Event-driven

Strategies subscribe to player events through the event bus and execute responsively.

Lifecycle management

Manages the start, stop, reset, and destruction of strategies.

Context injection

Strategies can access player status and data through the context.

Factory pattern

Supports global registration of strategy factories, which are automatically applied to all player instances.

Built-in strategies

AliPlayerKit provides three out-of-the-box built-in strategies.

Time-to-first-frame (TTFF) policy (FirstFrameStrategy)

This strategy calculates the time it takes for the player to go from the PREPARING stage to the rendering of the first frame. It is divided into three phases:

Phase

Description

Preparing

PREPARING → Prepared

Rendering

Prepared → FirstFrameRendered

Total time

PREPARING → FirstFrameRendered

Scenarios: Performance monitoring, user experience optimization, and player tuning.

Stuttering detection policy (StutterDetectStrategy)

This strategy monitors stuttering during playback and collects stuttering statistics:

Metric

Description

Stutter count

The number of times stuttering occurs.

Stutter duration

The duration of each stuttering event.

Total stutter duration

The total duration of stuttering for the entire playback session.

Effective playback duration

The actual viewing time, excluding pauses and stuttering.

Scenarios: Playback quality monitoring and user experience analysis.

Traffic Protection Policy (TrafficProtectionStrategy)

This strategy listens for network status changes and notifies users in specific scenarios:

Scenario

Behavior

Switching from Wi-Fi to a mobile network

Notifies the user that playback is now using mobile data.

Starting playback on a mobile network

Notifies the user that mobile data is being used.

Scenarios: Traffic-sensitive scenarios and user-friendly notifications.

Basic usage

The Strategy System provides three ways to use strategies. Choose the one that best fits your needs:

Policy

Description

Scenario

Use default strategies

The simplest method. The player component automatically registers default strategies.

Quick integration and standard playback scenarios.

Customize some strategies

Register only specific strategies and do not use others.

Partial customization and enabling strategies as needed.

Fully customize strategies

Customize all strategies to create a fully personalized monitoring system.

Deep customization and specific business requirements.

Method 1: Use built-in strategies

This is the simplest method. The player component automatically registers the default strategies:

// 1. Create a player controller. Default strategies are registered automatically.
AliPlayerController controller = new AliPlayerController(this);

// 2. Prepare the playback data.
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();

// 3. Configure the data model.
controller.configure(model);

// 4. Attach the controller to the view.
playerView.attach(controller);

The default registered strategies include the following:

  • FirstFrameStrategy (TTFF)

  • StutterDetectStrategy (stuttering detection)

  • TrafficProtectionStrategy (traffic protection)

Method 2: Customize strategies

Register only the strategies you need and disable the rest:

// 1. Create a player controller.
AliPlayerController controller = new AliPlayerController(this);

// 2. Get the strategy manager and clear the default strategies.
StrategyManager strategyManager = controller.getStrategyManager();

// 3. Register only the required strategies.
strategyManager.register(new FirstFrameStrategy());
strategyManager.register(new StutterDetectStrategy());

// 4. Prepare the playback data.
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();
controller.configure(model);
playerView.attach(controller);

Method 3: Fully customize strategies

Implement custom policies to create a fully personalized monitoring system:

// 1. Create a player controller.
AliPlayerController controller = new AliPlayerController(this);

// 2. Get the strategy manager.
StrategyManager strategyManager = controller.getStrategyManager();

// 3. Register custom policies.
strategyManager.register(new MyCustomStrategy());
strategyManager.register(new MyAnalyticsStrategy());

// 4. Prepare the playback data.
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();
controller.configure(model);
playerView.attach(controller);

Advanced usage

How to implement a custom policy

AliPlayerKit provides two implementation methods. Choose one based on your needs:

Method

Features

Scenario

Inherit BaseStrategy

Less code. The framework manages event subscriptions.

Most monitoring strategies.

Implement the IStrategy interface

High flexibility. Full autonomous control.

Requires special control logic.

Method 1: Inherit from BaseStrategy (Recommended)

Inheriting from BaseStrategy is the simplest method because the framework has already encapsulated event subscription and lifecycle management.

Scenario: Most monitoring and analysis strategies, such as statistics, detection, and logging.

  1. Create a strategy class.

    Inherit from BaseStrategy and implement the necessary methods:

    public class MyAnalyticsStrategy extends BaseStrategy {
    
        private static final String TAG = "MyAnalyticsStrategy";
    
        @NonNull
        @Override
        public String getName() {
            return TAG;  // Return the policy name for logging and debugging.
        }
    
        @Nullable
        @Override
        protected List<Class<? extends PlayerEvent>> observedEvents() {
            // Declare the event types to subscribe to.
            return Arrays.asList(
                PlayerEvents.StateChanged.class,
                PlayerEvents.Prepared.class,
                PlayerEvents.Info.class
            );
        }
    
        @Override
        public void onEvent(@NonNull PlayerEvent event) {
            // Filter out events from other players.
            if (!isCurrentPlayer(event)) return;
    
            // Process the event.
            if (event instanceof PlayerEvents.StateChanged) {
                handleStateChanged((PlayerEvents.StateChanged) event);
            } else if (event instanceof PlayerEvents.Prepared) {
                handlePrepared((PlayerEvents.Prepared) event);
            } else if (event instanceof PlayerEvents.Info) {
                handleInfo((PlayerEvents.Info) event);
            }
        }
    
        @Override
        public void onReset() {
            super.onReset();
            // Reset the internal state to prepare for a new video.
        }
    
        private void handleStateChanged(PlayerEvents.StateChanged event) {
            // Handle state changes.
        }
    
        private void handlePrepared(PlayerEvents.Prepared event) {
            // Handle preparation completion.
        }
    
        private void handleInfo(PlayerEvents.Info event) {
            // Handle playback information updates.
        }
    }
  2. Get started.

    StrategyManager strategyManager = controller.getStrategyManager();
    strategyManager.register(new MyAnalyticsStrategy());

Example: For more information, see strategies/FirstFrameStrategy.java.

Method 2: Implement the IStrategy interface

Directly implementing the IStrategy interface provides greater flexibility, but you need to handle event subscriptions yourself.

Scenario: You need full control over the strategy's behavior or require special initialization logic.

  1. Create a strategy class.

    Implement the IStrategy interface and manage event subscriptions yourself:

    public class MyCustomStrategy implements IStrategy, PlayerEventBus.EventListener<PlayerEvent> {
    
        private static final String TAG = "MyCustomStrategy";
    
        private StrategyContext mContext;
    
        @NonNull
        @Override
        public String getName() {
            return TAG;
        }
    
        @Override
        public void onStart(@NonNull StrategyContext context) {
            mContext = context;
            // Manually subscribe to events.
            PlayerEventBus.getInstance().subscribe(PlayerEvents.StateChanged.class, this);
        }
    
        @Override
        public void onStop() {
            // Manually unsubscribe from events.
            PlayerEventBus.getInstance().unsubscribe(PlayerEvents.StateChanged.class, this);
            mContext = null;
        }
    
        @Override
        public void onReset() {
            // Reset the internal state.
        }
    
        @Override
        public void onEvent(@NonNull PlayerEvent event) {
            // Process the event.
        }
    }
  2. Register for an account.

    StrategyManager strategyManager = controller.getStrategyManager();
    strategyManager.register(new MyAnalyticsStrategy());

How to implement a strategy with callbacks

Strategies can use callback interfaces to notify the business layer of results:

public class MyAnalyticsStrategy extends BaseStrategy {

    public interface Callback {
        void onAnalysisComplete(AnalysisResult result);
    }

    @Nullable
    private final Callback mCallback;

    // Support a parameterless constructor.
    public MyAnalyticsStrategy() {
        this(null);
    }

    // Support passing a callback.
    public MyAnalyticsStrategy(@Nullable Callback callback) {
        mCallback = callback;
    }

    @Override
    public void onEvent(@NonNull PlayerEvent event) {
        // ... business logic

        // Notify the result through the callback.
        if (mCallback != null) {
            mCallback.onAnalysisComplete(result);
        }
    }
}

Usage:

strategyManager.register(new MyAnalyticsStrategy(result -> {
    // Process the analysis result.
    Log.d("Analytics", "Result: " + result);
}));

Example: For more information, see strategies/FirstFrameStrategy.java.

How to access player status

Strategies can access player status and data through StrategyContext:

@Override
public void onEvent(@NonNull PlayerEvent event) {
    if (!isCurrentPlayer(event)) return;

    // Get the player ID.
    String playerId = getPlayerId();

    // Get the playback data model.
    AliPlayerModel model = mContext.getModel();
    if (model != null) {
        String title = model.getVideoTitle();
        VideoSource source = model.getVideoSource();
    }

    // Get the player state store.
    IPlayerStateStore stateStore = mContext.getPlayerStateStore();
    long position = stateStore.getCurrentPosition();
    long duration = stateStore.getDuration();
    PlayerState state = stateStore.getPlayState();
}
Important

mContext is null in the constructor. It is initialized only after onStart() is called.

How to register a global strategy

You can use StrategyRegistry to register a global strategy. It will be automatically applied to all newly created player instances:

// Register a global strategy.
StrategyRegistry.addGlobalStrategy(() -> new MyAnalyticsStrategy());

// All AliPlayerController instances created after this will automatically register this strategy.
AliPlayerController controller = new AliPlayerController(this);   

Restore default configurations:

StrategyRegistry.resetToDefault();

Best practices

Lifecycle management

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);  // Must be called.
        // Initialize resources.
    }

    @Overridepublic void onStop() {
        // Clean up resources.
        super.onStop();  // Must be called.
    }

    @Overridepublic void onReset() {
        super.onReset();
        // Reset the internal state to prepare for a new video.
    }
}

Event filtering

In multi-player scenarios, always filter the event source:

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // Filter out events from other players to avoid interference.
    if (!isCurrentPlayer(event)) return;

    // Process events for the current player.
}

Resource cleanup

If a strategy has asynchronous resources such as a Handler or Runnable, clean them up in onStop():

public class MyStrategy extends BaseStrategy {

    private final Handler mHandler = new Handler();
    private Runnable mRunnable;

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);
        mRunnable = () -> doSomething();
        mHandler.postDelayed(mRunnable, 1000);
    }

    @Overridepublic void onStop() {
        // Clean up asynchronous tasks to avoid memory leaks.
        mHandler.removeCallbacks(mRunnable);
        super.onStop();
    }
}

Notes

Scenario

Recommended practice

Reason

Multi-player scenarios

Use isCurrentPlayer() to filter events.

Avoid event interference.

Long-running tasks

Cancel tasks in onStop().

Avoid memory leaks.

Video switching

Reset the state in onReset().

Ensure the state is correct.

Accessing Context

Obtain it through mContext.getModel().

Ensure the context is available.

Example reference

The project provides a complete example located at playerkit-examples/example-strategy-system.

Example features

Feature

Description

Resume playback strategy

Automatically records playback progress and resumes from that point next time.

Strategy registration demo

Demonstrates how to register a custom policy.

Run the example

In the demo app, select the Strategy System example to see it in action.

API reference

Core interfaces

Interface/Class

Description

IStrategy

The strategy interface. It defines lifecycle methods.

BaseStrategy

The base class for strategies. It encapsulates event subscription management.

StrategyManager

The strategy manager. It manages the strategy lifecycle.

StrategyContext

The strategy context. It provides read-only player information.

StrategyRegistry

The service registry for strategies. It manages global strategies.

BaseStrategy methods

Method

Description

getName()

Returns the policy name for logging and debugging.

observedEvents()

Returns a list of event types to subscribe to.

onEvent(event)

The event callback. It processes subscribed events.

isCurrentPlayer(event)

Checks if the event is from the current player.

getPlayerId()

Gets the current player ID.

StrategyManager methods

Method

Description

register(strategy)

Registers a strategy.

unregister(strategy)

Unregisters a strategy.

getStrategy(name)

Gets a strategy by name.

start(context)

Starts all strategies.

stop()

Stops all strategies.

reset()

Resets all strategies.

destroy()

Destroys the manager.

StrategyContext methods

Method

Description

getPlayerId()

Gets the player ID.

getModel()

Gets the playback data model.

getPlayerStateStore()

Gets the player state store (read-only).

Technical principles

Overall architecture

The Strategy System uses an event-driven and manager-scheduled architectural pattern.

Design points:

  • Strategies do not hold player references: Complete decoupling is achieved through an event-driven design.

  • Strategies only focus on events: They only need to declare what to subscribe to and how to handle it.

  • The manager handles scheduling: The manager controls the lifecycle and event dispatching.

Lifecycle

Strategies use a three-stage lifecycle:

Lifecycle

Trigger Condition

Description

onStart(context)

playerView.attach()

The strategy starts, subscribes to events, and initializes resources.

onReset()

Playback content switches

Resets the internal state to prepare for a new video.

onStop()

playerView.detach() or destruction

The strategy stops, unsubscribes, and cleans up resources.

Event-driven mechanism

Strategies subscribe to player events through an event bus, using a publish-subscribe pattern.

Event subscription flow:

  1. The strategy declares the event types it needs to subscribe to in observedEvents().

  2. BaseStrategy automatically subscribes to these events in onStart().

  3. BaseStrategy automatically unsubscribes in onStop().

  4. The strategy processes the received events in onEvent().

Design philosophy: The event-driven mechanism completely decouples strategies from the player. Strategies do not need to hold a player reference. They only need to focus on event subscription and handling logic. This achieves a separation of concerns.

Multi-player isolation

In multi-player scenarios, the Strategy System ensures isolation through the following mechanisms:

  1. Independent StrategyManager: Each AliPlayerController has its own independent StrategyManager.

  2. Independent StrategyContext: Each strategy context is bound to a specific player ID.

  3. Event filtering: Strategies filter event sources using isCurrentPlayer(event).

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // Events from each player instance carry a playerId.
    // isCurrentPlayer() checks if the event is from the current player.
    if (!isCurrentPlayer(event)) return;

    // Process only events from the current player.
}

FAQ

When does a strategy start?

All registered strategies start automatically when you call playerView.attach().

How do I get the current player status?

Use the getPlayerStateStore() method of StrategyContext:

IPlayerStateStore stateStore = mContext.getPlayerStateStore();
PlayerState state = stateStore.getPlayState();
long position = stateStore.getCurrentPosition();

How do I debug a strategy?

Use LogHub to view logs. The TAG format is Policy Name.BaseStrategy or StrategyManager.

Common mistakes that cause crashes

The following are the most common issues reported by customers that lead to crashes. Avoid these mistakes.

Mistake 1: Accessing mContext in the constructor

Incorrect code:

public class MyStrategy extends BaseStrategy {

    public MyStrategy() {
        super();
        // mContext is null in the constructor.
        String playerId = getPlayerId();  // Returns an empty string.
        AliPlayerModel model = mContext.getModel();  // NullPointerException!
    }
}

Correct code:

@Overridepublic void onStart(@NonNull StrategyContext context) {
    super.onStart(context);  // ✅ Must be called first.
    // mContext can be accessed after onStart.
    String playerId = getPlayerId();  // Gets the ID normally.
    AliPlayerModel model = mContext.getModel();  // Gets the model normally.
}

Reason for crash: mContext is assigned after onStart() is called. The strategy has not started yet when the constructor executes.

Mistake 2: Forgetting to call super.onStart() causes event subscription to fail

Incorrect code:

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        // Forgot to call super.onStart(context).
        // mContext will be null, and event subscription will not execute.
        initMyResources();
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // This will never be called because event subscription did not execute.
    }
}

Correct code:

@Overridepublic void onStart(@NonNull StrategyContext context) {
    super.onStart(context);  // ✅ Must be called first.
    initMyResources();
}

Reason for crash: super.onStart(context) initializes mContext and subscribes to the events returned by observedEvents(). Not calling it will result in a null mContext and invalid event subscriptions.

Mistake 3: Not filtering events causes interference in multi-player scenarios

Incorrect code:

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
            // The event source is not filtered, which causes crosstalk in multi-player scenarios.
            if (event instanceof PlayerEvents.StateChanged) {
            // State changes from all players will trigger this logic.
            updateUI();  // This may update the UI of the wrong player.
        }
    }
}

Correct code:

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // First, filter out events that are not from the current player. if (!isCurrentPlayer(event)) return;

    if (event instanceof PlayerEvents.StateChanged) {
        updateUI();  // Only process events from the current player.
    }
}

Cause of crash: In a multi-player scenario, events from all players are dispatched to all policies. Failure to filter causes policies to process events from other players.

Mistake 4: Not cleaning up asynchronous tasks in onStop() causes a memory leak

Incorrect code:

public class MyStrategy extends BaseStrategy {

    private Handler handler = new Handler();

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);
        handler.postDelayed(() -> updateProgress(), 1000);  // Delayed task.
    }

    @Overridepublic void onStop() {
        // Forgot to remove the delayed task.
        super.onStop();
    }
}

Correct code:

@Overridepublic void onStop() {
    handler.removeCallbacksAndMessages(null);  // Clear all delayed tasks
    super.onStop();
}

Cause of crash: A delayed task holds a reference to a policy instance. After the policy is stopped, the instance cannot be garbage collected, which causes a memory leak. When the task executes, it may access resources that have already been destroyed.

Mistake 5: observedEvents returns null, causing event subscription to fail

Incorrect code:

public class MyStrategy extends BaseStrategy {

    @Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        // Returning null will not cause a crash, but the event subscription will not be executed.
        return null;
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // Will never be called.
    }
}

Correct code:

@Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
    return Arrays.asList(
        PlayerEvents.StateChanged.class,  // Declare the events to subscribe to
        PlayerEvents.Prepared.class
    );
}

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // You can now receive events.
}

Reason for crash: BaseStrategy checks the return value of observedEvents() when subscribing to events. If the return value is `null` or an empty list, it does not subscribe to any events.

Mistake 6: Not resetting the internal state in onReset() leads to incorrect data

Incorrect code:

public class MyStrategy extends BaseStrategy {

    private int mPlayCount = 0;

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        if (!isCurrentPlayer(event)) return;

        if (event instanceof PlayerEvents.Prepared) {
            mPlayCount++;  // The count is not reset when the video is switched, so it accumulates.
            Log.d(TAG, "Play count: " + mPlayCount);
        }
    }

    @Overridepublic void onReset() {
        super.onReset();
        // Forgot to reset mPlayCount.
    }
}

Correct code:

@Overridepublic void onReset() {
    super.onReset();
    mPlayCount = 0;  // Reset the internal state.
}

Cause of crash: onReset() is called during video switching to reset the internal state of the policy. If the state is not reset, it accumulates and affects statistical accuracy.