Event system

更新时间:
复制 MD 格式

This topic introduces the core concepts, event categories, usage methods, and best practices of the AliPlayerKit event system.

Concepts

What is an event?

An event is a notification of a state change or behavior that occurs during player runtime. Each event carries a player ID (playerId) to identify its source and isolate events across multiple player instances.

Events in AliPlayerKit fall into two main categories:

Type

Description

Example

Status event

A notification of player state changes, generated internally by the player.

StateChanged, Prepared, Error

Command event

An instruction to control player behavior, generated by UI or external components.

Play, Pause, Seek, SetSpeed

All events inherit from the PlayerEvent base class to ensure a consistent event structure.

What is the event system?

The event system is a communication architecture for managing event subscription and publishing. Its core component is PlayerEventBus.

It provides the following capabilities:

  • Type-safe subscription: You can subscribe by event type. Type safety is guaranteed at compile time.

  • Thread safety: Supports event publishing and subscription in multi-threaded environments.

  • Weak reference management: Stores listeners using weak references and automatically cleans up listeners that have been garbage-collected.

  • Event isolation: Isolates events across multiple player instances using playerId.

The event system fully decouples UI components (Slots) from controllers. The UI only sends commands and subscribes to state changes, without concern for how commands are executed. Controllers only handle commands and publish state changes, without concern for who is listening.

Features

Problems solved

  1. Component decoupling: Removes direct dependencies between components, which reduces coupling and improves replaceability and testability.

  2. Event traceability: Resolves event source confusion in multi-player scenarios.

  3. Separation of responsibilities: Separates UI components from business logic and clarifies responsibility boundaries.

Core value

The event system standardizes component communication. Developers can use it in the following ways:

Usage method

Description

Advantage

Subscribe to events

Listen for player state changes.

Decouples UI from business logic and updates the UI reactively.

Send commands

Control player behavior.

The UI does not need to hold a controller reference. Interaction is command-based.

Customize events

Extend event types.

Fits specific business scenarios while maintaining architectural consistency.

Architectural advantages:

  • Decoupling: Components communicate through events and have no direct dependencies.

  • Testability: You can test components independently by simulating interactions with events.

  • Extensibility: You can add custom event types without modifying the framework code.

  • Thread safety: Works in multi-threaded environments without requiring extra synchronization.

Core capabilities

Capability

Description

Type-safe subscription

Subscribe by event type. Type checking happens at compile time.

Weak reference management

Automatically cleans up listeners that have been garbage-collected to prevent memory leaks.

Event inheritance support

Subscribing to a parent event type receives all child event types.

Multi-player isolation

Distinguishes events from different players using playerId.

Event categories

Player events (PlayerEvents)

These are status change events generated inside the player to notify external components of the player's state.

Event

Description

Data carried

Prepared

The player is ready.

duration: total video duration

FirstFrameRendered

The first frame has rendered.

-

StateChanged

The playback state changed.

oldState, newState

VideoSizeChanged

The video size changed.

width, height

Info

The playback progress updated.

currentPosition, duration, bufferedPosition

Error

A playback error occurred.

errorCode, errorMsg

LoadingBegin

Buffering started.

-

LoadingProgress

Buffering progress.

percent, netSpeed

LoadingEnd

Buffering ended.

-

SetSpeedCompleted

Playback speed setting completed.

speed

SnapshotCompleted

Screenshot completed.

result, snapshotPath, width, height

SetLoopCompleted

Loop setting completed.

loop

SetMuteCompleted

Mute setting completed.

mute

SetScaleTypeCompleted

Fill mode setting completed.

scaleType

SetMirrorTypeCompleted

Mirror setting completed.

mirrorType

SetRotationCompleted

Rotation setting completed.

rotation

TrackQualityListUpdated

The definition list updated.

trackQualityList

TrackSelected

Definition selection completed.

trackIndex

Player commands (PlayerCommand)

These are command events used to control player behavior. They are sent by UI components or external code.

Command

Description

Parameters

Play

Start playback.

-

Pause

Pause playback.

-

Toggle

Toggle between play and pause.

-

Stop

Stop playback.

-

Replay

Replay.

-

Seek

Jump to a specified position.

position: target position in milliseconds

SetSpeed

Set playback speed.

speed: playback speed (0.3–3.0)

Snapshot

Take a screenshot.

-

SetLoop

Set loop playback.

loop: whether to loop

SetMute

Set mute.

mute: whether to mute

SetScaleType

Set fill mode.

scaleType: fill mode

SetMirrorType

Set mirror.

mirrorType: mirror mode

SetRotation

Set rotation.

rotationMode: rotation angle

SelectTrack

Switch definition.

trackQuality: target definition

Gesture events (GestureEvents)

Events generated by user gesture operations. These events describe the gesture itself and contain no business logic.

Event

Description

Data carried

SingleTapEvent

Single tap.

x, y: tap coordinates

DoubleTapEvent

Double tap.

x, y: tap coordinates

LongPressEvent

Press and hold to start.

x, y: long press coordinates

LongPressEndEvent

Long press ended.

-

HorizontalDragStartEvent

Horizontal drag started.

startX, startY

HorizontalDragUpdateEvent

Horizontal drag updated.

deltaPercent: percentage increment

HorizontalDragEndEvent

Horizontal drag ended.

-

LeftVerticalDragStartEvent

Left vertical drag started.

startX, startY

LeftVerticalDragUpdateEvent

Left vertical drag updated.

deltaPercent, currentPercent

LeftVerticalDragEndEvent

Left vertical drag ended.

-

RightVerticalDragStartEvent

Right vertical drag started.

startX, startY

RightVerticalDragUpdateEvent

Right vertical drag updated.

deltaPercent, currentPercent

RightVerticalDragEndEvent

Right vertical drag ended.

-

Control bar events (ControlBarEvents)

Events that synchronize the control bar display state.

Event

Description

Show

Show the control bar.

Hide

Hide the control bar.

ResetTimer

Reset the auto-hide timer.

ShowSettings

Show the settings interface.

Fullscreen events (FullscreenEvents)

These are events for switching to and from fullscreen mode.

Event

Description

Toggle

Toggle fullscreen mode.

Lifecycle events (PlayerLifecycleEvents)

These are events related to player lifecycle policies. You can use them to observe player instance creation, reuse, and destruction.

Event

Description

PlayerCreated

Create a new player instance.

PlayerDestroyed

Destroy a player instance.

PlayerReused

Reuse an idle player instance.

PlayerHit

Hit an active player instance.

PlayerEvicted

A player instance was evicted.

Slot events (SlotEvents)

These are events generated inside slots.

Event

Description

TopBarBackClicked

Click the back button in the top bar.

Basic usage

The event system supports two usage strategies:

Strategy

Description

Use case

Subscribe to events

Listen for player state changes.

UI updates and business logic responses.

Send commands

Control player behavior.

User interaction and external control.

Strategy one: Subscribe to events

Subscribing to events requires three steps: obtain the event bus, create a listener, and subscribe to events.

public class MyActivity extends AppCompatActivity {

    private PlayerEventBus mEventBus;
    private PlayerEventBus.EventListener<PlayerEvents.Info> mInfoListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 1. Get the singleton event bus
        mEventBus = PlayerEventBus.getInstance();

        // 2. Create a listener
        mInfoListener = event -> {
            // Handle playback progress updates
            long position = event.currentPosition;
            long duration = event.duration;
            updateProgressUI(position, duration);
        };

        // 3. Subscribe to events
        mEventBus.subscribe(PlayerEvents.Info.class, mInfoListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 4. Unsubscribe to avoid memory leaks
        if (mEventBus != null && mInfoListener != null) {
            mEventBus.unsubscribe(PlayerEvents.Info.class, mInfoListener);
        }
    }
}     

Strategy two: Send commands

You can send commands using the post() method on the event bus:

// 1. Get the singleton event bus
PlayerEventBus eventBus = PlayerEventBus.getInstance();

// 2. Get the target player ID
// [Scenario A: Inside a slot] Use the built-in mechanism
// String playerId = getPlayerId();
// [Scenario B: In an Activity or Fragment] Get it from your controller
String playerId = mAliPlayerKitController.getPlayer().getPlayerId();

// 3. Post a command (the controller mounted with this playerId will execute it)
eventBus.post(new PlayerCommand.Play(playerId));

// Other common command examples:
eventBus.post(new PlayerCommand.Pause(playerId));               // Pause playback
eventBus.post(new PlayerCommand.Toggle(playerId));              // Toggle play/pause
eventBus.post(new PlayerCommand.Seek(playerId, 30000));         // Jump to 30 seconds
eventBus.post(new PlayerCommand.SetSpeed(playerId, 1.5f));      // Set playback speed to 1.5×          

Advanced usage

How to use events inside a slot

You can use events inside a slot in two ways: subscribe to events or send commands.

Method one: Subscribe to events (recommended: use observedEvents())

The BaseSlot class provides the observedEvents() and onEvent() methods. The framework manages the subscription lifecycle automatically.

public class MySlot extends BaseSlot {

    // 1. Declare the event types to subscribe to
    @Override
    protected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(
            PlayerEvents.StateChanged.class,
            PlayerEvents.Info.class
        );
    }

    // 2. Handle event callbacks
    @Override
    protected void onEvent(@NonNull PlayerEvent event) {
        if (event instanceof PlayerEvents.StateChanged) {
            PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
            updatePlayPauseIcon(stateChanged.newState);
        } else if (event instanceof PlayerEvents.Info) {
            PlayerEvents.Info info = (PlayerEvents.Info) event;
            updateProgress(info.currentPosition, info.duration);
        }
    }
}          

Method two: Send commands

Inside a slot, you can send commands using the postEvent() method:

public class MySlot extends BaseSlot {

    private void onPlayPauseClick() {
        // Get the playerId (available after onAttach)
        String playerId = getPlayerId();
        if (playerId != null) {
            // Send the toggle play/pause command
            postEvent(new PlayerCommand.Toggle(playerId));
        }
    }

    private void onSeekTo(long position) {
        String playerId = getPlayerId();
        if (playerId != null) {
            // Send the seek command
            postEvent(new PlayerCommand.Seek(playerId, position));
        }
    }
}      

For more information about using slots, see Slot system.

How to use events inside a strategy

Using events inside a strategy is similar to using them inside a slot. You can use observedEvents() and onEvent() to subscribe to and handle events. Strategies have the following characteristics:

Characteristic

Description

Mainly for monitoring

Strategies usually only subscribe to events for analysis and do not send commands.

Filter events

In multi-player scenarios, you must filter events using isCurrentPlayer(event).

Access context

Access player state and data using StrategyContext.

Example code:

public class MyAnalyticsStrategy extends BaseStrategy {

    @Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(
            PlayerEvents.StateChanged.class,
            PlayerEvents.Info.class
        );
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // Must filter event source to avoid cross-talk in multi-player scenarios
        if (!isCurrentPlayer(event)) return;

        if (event instanceof PlayerEvents.StateChanged) {
            // Handle state changes
            PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
            logState(stateChanged.newState);
        } else if (event instanceof PlayerEvents.Info) {
            // Handle playback progress
            PlayerEvents.Info info = (PlayerEvents.Info) event;
            trackProgress(info.currentPosition, info.duration);
        }
    }
}

For more information about using strategies, see Strategy system.

How to implement custom events

If your business requires custom events, you can extend PlayerEvent to create a new event type.

  1. Create an event class.

    Extend PlayerEvent and add your business-specific data fields.

    /**
     * Custom playback event
     */public class MyCustomEvent extends PlayerEvent {
    
        /**
         * Custom data field
         */public final String customData;
    
        /**
         * Constructor
         *
         * @param playerId Player ID
         * @param customData Custom data
         */public MyCustomEvent(@NonNull String playerId, @NonNull String customData) {
            super(playerId);
            this.customData = customData;
        }
    }
  2. Publish the event.

    // Create and post the event
    MyCustomEvent event = new MyCustomEvent(playerId, "custom_data");
    PlayerEventBus.getInstance().post(event);
  3. Subscribe to the custom event.

    // Subscribe to the custom event
    PlayerEventBus.getInstance().subscribe(MyCustomEvent.class, event -> {
        // Handle the custom event
        String data = event.customData;
    });

How to achieve multi-player event isolation

Each event carries a playerId. You can compare the playerId to isolate events across multiple players.

// Filter playerId when subscribing to events
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // Only handle events from the current player
    if (mPlayerId.equals(event.playerId)) {
        updateProgress(event.currentPosition, event.duration);
    }
});

How to subscribe to commands in bulk

The PlayerCommand class provides the ALL_COMMANDS array for bulk subscriptions.

// Subscribe to all playback commands
for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
    mEventBus.subscribe(commandClass, mCommandListener);
}

// Unsubscribe from all playback commands
for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
    mEventBus.unsubscribe(commandClass, mCommandListener);
}

Best practices

Subscription lifecycle management

Scenario

Recommended practice

Reason

Activity or Fragment

Unsubscribe in onDestroy().

Prevents Activity leaks.

Inside a slot

Use observedEvents() for automatic management.

The framework handles the lifecycle automatically.

Singleton component

Use weak references or manage manually.

Singletons live longer, so cleanup must be explicit.

Thread safety

Events are dispatched synchronously on the calling thread. Note the following cases:

// Subscribing on the main thread means callbacks also run on the main thread
runOnUiThread(() -> {
    mEventBus.subscribe(PlayerEvents.Info.class, event -> {
        // This callback runs on the thread that posted the event.
        // If the event was posted from a subthread, switch to the main thread to update the UI.
        runOnUiThread(() -> updateUI(event));
    });
});

Command sending timing

Command

Recommended timing

Description

Play/Pause/Toggle

Any time.

Safe commands with no side effects.

Seek

After the player is Prepared.

Ensures the video is loaded.

SetSpeed

During playback or pause.

Ensures the player is initialized.

Snapshot

After the first frame renders.

Ensures there is a frame to capture.

Important notes

  1. Constructor risk: Do not subscribe to or send events in constructors. The object is not fully initialized, which may cause null pointer exceptions. The playerId is not yet injected.

  2. Listener matching: When unsubscribing, use the original listener instance. Matching relies on object reference equality.

  3. Avoid long-running operations: Do not perform long-running operations in event callbacks. Events are dispatched synchronously and will block the thread.

High-Frequency Events Performance Threshold:

  • Scenarios: High-frequency events such as PlayerEvents.Info (playback progress), LoadingProgress, and GestureEvents.Update (up to dozens per second).

  • Avoid memory allocation (such as new String()) by reusing objects. To prevent heavy layout refreshes and reduce repaints, use a throttling policy and same-value filtering.

Examples

The project includes a complete example at playerkit-examples/example-event-system.

Example features

Feature

Description

Subscribe to playback progress

Receive PlayerEvents.Info events in real time.

Send playback commands

Control play/pause using PlayerCommand.Toggle.

Display event logs

Show the last 20 event messages.

Run the example

In the demo app, you can select the Event System example to view it.

API reference

Core interfaces

Interface or class

Description

PlayerEvent

Base class for all events.

PlayerCommand

Base class for all commands.

PlayerEventBus

Event bus that manages subscriptions and publishing.

EventListener<T>

Event listener interface.

PlayerEventBus methods

Method

Description

getInstance()

Get the singleton event bus.

subscribe(eventType, listener)

Subscribe to events of a specified type.

unsubscribe(eventType, listener)

Unsubscribe from events of a specified type.

unsubscribe(listener)

Unsubscribe a listener from all event types.

post(event)

Publish an event.

getSubscriberCount(eventType)

Get the number of subscribers for a specified event type.

hasSubscribers(eventType)

Check whether any subscribers exist for a specified event type.

unsubscribeAll()

Clear all subscriptions (use with caution).

Technical principles

Weak reference mechanism

The event bus stores listeners using WeakReference. This offers the following advantages:

  • Automatic cleanup: Listeners are removed from the subscription list after garbage collection.

  • Leak prevention: Serious memory leaks will not occur even if you forget to unsubscribe.

  • Safe access: Before dispatching, the event bus checks whether each weak reference is still valid.

// Store listeners using weak references
List<WeakReference<EventListener<? extends PlayerEvent>>> listeners;

// Check validity before dispatching
for (WeakReference<EventListener> ref : listeners) {
    EventListener listener = ref.get();
    if (listener != null) {
        listener.onEvent(event);  // Call if valid
    } else {
        listeners.remove(ref);  // Clean up if invalid
    }
}

Unidirectional data flow and state sync loop

The event system handles only transient notifications. When combined with the PlayerStateStore for state persistence, it forms a strict unidirectional data flow (UDF) architecture.

Data flow:

Direction

Mechanism

Description

Command up

UI → Controller

A slot sends command events to control player behavior.

State down

Controller → UI

The controller publishes state change events to notify slots.

State pull

UI → StateStore

A slot actively pulls the current state (a fallback mechanism).

Why is state pull needed?

Events are transient. A late-bound slot may miss historical events after it is attached. Using getPlayerStateStore(), you can instantly sync the player's current state without relying on event subscription timing.

@Overridepublic void onAttach(@NonNull SlotHost host) {
    super.onAttach(host);

    // Did the slot attach late and miss Prepared, StateChanged, etc.?
    // Pull the current state to initialize the UI
    IPlayerStateStore stateStore = host.getPlayerStateStore();
    PlayerState currentState = stateStore.getPlayState();  // Current playback state
    long position = stateStore.getCurrentPosition();       // Current playback position
    long duration = stateStore.getDuration();              // Total video duration

    // Initialize the UI based on the current state
    updatePlayPauseIcon(currentState);
    updateProgress(position, duration);
}

This architecture fully decouples the UI from the controller. The UI does not hold a controller reference. It only subscribes to events and sends commands. The controller does not care who is listening and only publishes events and handles commands.

Event inheritance support

If you subscribe to a parent event type, you will receive all child event types.

// Subscribe to PlayerEvent to receive all events
mEventBus.subscribe(PlayerEvent.class, event -> {
    // Receive all PlayerEvent subclasses
});

// Subscribe to PlayerCommand to receive all commands
mEventBus.subscribe(PlayerCommand.class, event -> {
    // Receive all PlayerCommand subclasses
});

FAQ

Why am I not receiving events?

You can follow these steps to troubleshoot the issue:

  1. Subscription status: Confirm that you called subscribe().

  2. Event matching: The subscribed type must match the published type.

  3. Identity check: The event's playerId must match.

  4. Garbage collection check: Verify that the listener has not been garbage-collected prematurely.

How do I debug events?

You can use LogHub to view logs. The tag is PlayerEventBus.

// View subscription logs
// I/PlayerEventBus: Subscribed to Info
// View publication logs (enable verbose logging)
// I/PlayerEventBus: Posted event: Info to 2 listeners

Common crash mistakes

The following are the most common issues that developers face. You should avoid them.

Mistake 1: Forgetting to unsubscribe causes memory leaks

Incorrect code:

public class MyActivity extends AppCompatActivity {

    @Overrideprotected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Subscribed but never unsubscribed
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
            updateUI(event.currentPosition);  // Activity leak!
        });
    }

    // No onDestroy to unsubscribe
}

Correct code:

public class MyActivity extends AppCompatActivity {

    private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;

    @Overrideprotected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mListener = event -> updateUI(event.currentPosition);
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    @Overrideprotected void onDestroy() {
        super.onDestroy();
        // ✅ Always unsubscribe
        PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    }
}

Crash reason: Lambda expressions hold a reference to the Activity. If you do not unsubscribe, the Activity cannot be released.

Mistake 2: Subscribing to events in constructors

Incorrect code:

public class MyComponent {

    public MyComponent() {
        // Subscribing in constructor—object not fully initialized
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
            // At this point, 'this' may not be fully initialized
        });
    }
}

Correct approach:

public class MyComponent {

    private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;

    public void init() {
        // Subscribe in an initialization method
        mListener = event -> handleEvent(event);
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    public void destroy() {
        // Unsubscribe
        PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    }
}

Crash reason: The object is not fully initialized during constructor execution, which may cause null pointer exceptions or inconsistent state.

Mistake 3: Sending events in slot constructors

Incorrect code:

public class MySlot extends BaseSlot {

    public MySlot(Context context) {
        super(context);
        // playerId not set in constructor
        postEvent(new PlayerCommand.Toggle(getPlayerId()));  // getPlayerId() returns null!
    }
}

Correct approach:

public class MySlot extends BaseSlot {

    @Overridepublic void onAttach(@NonNull SlotHost host) {
        super.onAttach(host);
        // Send events after onAttach
        String playerId = getPlayerId();
        if (playerId != null) {
            postEvent(new PlayerCommand.Toggle(playerId));
        }
    }
}

Crash reason: The getPlayerId() method returns a valid value only after the onAttach() method is called. It returns null in the constructor.

Mistake 4: Performing long-running operations in event callbacks

Incorrect code:

mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // Network request in callback blocks event dispatch
    String data = fetchFromNetwork();  // Blocks!
    updateUI(data);
});

Correct approach:

mEventBus.subscribe(PlayerEvents.Info.class, event -> {
    // Run long-running operations asynchronously
    executorService.execute(() -> {
        String data = fetchFromNetwork();
        runOnUiThread(() -> updateUI(data));
    });
});

Crash reason: Event dispatch is synchronous. Long-running operations block all subsequent event dispatches, which causes UI stuttering.

Mistake 5: Not using observedEvents() in slots

Incorrect code:

public class MySlot extends BaseSlot {

    @Overridepublic void onAttach(@NonNull SlotHost host) {
        super.onAttach(host);
        // Manual subscription requires manual cleanup
        PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
    }

    @Overridepublic void onDetach() {
        // Forgot to unsubscribe—leak!
        super.onDetach();
    }
}

Correct approach:

public class MySlot extends BaseSlot {

    // Use observedEvents() for automatic management
    @Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        return Arrays.asList(PlayerEvents.Info.class);
    }

    @Overrideprotected void onEvent(@NonNull PlayerEvent event) {
        if (event instanceof PlayerEvents.Info) {
            // Handle event
        }
    }
}

Crash reason: Manual subscription requires manual cleanup. If you forget to unsubscribe, listener leaks will occur.

If you must use manual subscription, you must always unsubscribe in the onDetach() method:

@Overridepublic void onDetach() {
    PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
    super.onDetach();
}