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. |
|
Command event | An instruction to control player behavior, generated by UI or external components. |
|
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
Component decoupling: Removes direct dependencies between components, which reduces coupling and improves replaceability and testability.
Event traceability: Resolves event source confusion in multi-player scenarios.
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 |
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 |
| The player is ready. |
|
| The first frame has rendered. | - |
| The playback state changed. |
|
| The video size changed. |
|
| The playback progress updated. |
|
| A playback error occurred. |
|
| Buffering started. | - |
| Buffering progress. |
|
| Buffering ended. | - |
| Playback speed setting completed. |
|
| Screenshot completed. |
|
| Loop setting completed. |
|
| Mute setting completed. |
|
| Fill mode setting completed. |
|
| Mirror setting completed. |
|
| Rotation setting completed. |
|
| The definition list updated. |
|
| Definition selection completed. |
|
Player commands (PlayerCommand)
These are command events used to control player behavior. They are sent by UI components or external code.
Command | Description | Parameters |
| Start playback. | - |
| Pause playback. | - |
| Toggle between play and pause. | - |
| Stop playback. | - |
| Replay. | - |
| Jump to a specified position. |
|
| Set playback speed. |
|
| Take a screenshot. | - |
| Set loop playback. |
|
| Set mute. |
|
| Set fill mode. |
|
| Set mirror. |
|
| Set rotation. |
|
| Switch 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 |
| Single tap. |
|
| Double tap. |
|
| Press and hold to start. |
|
| Long press ended. | - |
| Horizontal drag started. |
|
| Horizontal drag updated. |
|
| Horizontal drag ended. | - |
| Left vertical drag started. |
|
| Left vertical drag updated. |
|
| Left vertical drag ended. | - |
| Right vertical drag started. |
|
| Right vertical drag updated. |
|
| Right vertical drag ended. | - |
Control bar events (ControlBarEvents)
Events that synchronize the control bar display state.
Event | Description |
| Show the control bar. |
| Hide the control bar. |
| Reset the auto-hide timer. |
| Show the settings interface. |
Fullscreen events (FullscreenEvents)
These are events for switching to and from fullscreen mode.
Event | Description |
| 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 |
| Create a new player instance. |
| Destroy a player instance. |
| Reuse an idle player instance. |
| Hit an active player instance. |
| A player instance was evicted. |
Slot events (SlotEvents)
These are events generated inside slots.
Event | Description |
| 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 |
Access context | Access player state and data using |
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.
Create an event class.
Extend
PlayerEventand 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; } }Publish the event.
// Create and post the event MyCustomEvent event = new MyCustomEvent(playerId, "custom_data"); PlayerEventBus.getInstance().post(event);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 | Prevents |
Inside a slot | Use | 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 |
| Any time. | Safe commands with no side effects. |
| After the player is | Ensures the video is loaded. |
| During playback or pause. | Ensures the player is initialized. |
| After the first frame renders. | Ensures there is a frame to capture. |
Important notes
Constructor risk: Do not subscribe to or send events in constructors. The object is not fully initialized, which may cause null pointer exceptions. The
playerIdis not yet injected.Listener matching: When unsubscribing, use the original listener instance. Matching relies on object reference equality.
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, andGestureEvents.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 |
Send playback commands | Control play/pause using |
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 |
| Base class for all events. |
| Base class for all commands. |
| Event bus that manages subscriptions and publishing. |
| Event listener interface. |
PlayerEventBus methods
Method | Description |
| Get the singleton event bus. |
| Subscribe to events of a specified type. |
| Unsubscribe from events of a specified type. |
| Unsubscribe a listener from all event types. |
| Publish an event. |
| Get the number of subscribers for a specified event type. |
| Check whether any subscribers exist for a specified event type. |
| 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:
Subscription status: Confirm that you called
subscribe().Event matching: The subscribed type must match the published type.
Identity check: The event's
playerIdmust match.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 listenersCommon 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();
}