A clean, modern Android video player SDK built on top of AndroidX Media3 (ExoPlayer). FastPix Player SDK provides a simple, SDK-friendly API for video playback with built-in support for configuration change survival, playback event tracking, and fullscreen mode.
- Built on Media3 (ExoPlayer) – Uses Google's powerful and reliable video playback engine
- FastPix URL Generator – Built-in builder pattern for creating FastPix media items with resolution, token, and streaming options
- Configuration Change Survival – Playback state is preserved across orientation changes and configuration updates (default behavior)
- Event-Driven Architecture – Comprehensive playback event listeners for time updates, seek operations, buffering, and errors
- Fullscreen Mode – Built-in fullscreen support with proper view reparenting and system UI handling
- Gesture Support – Single-tap to toggle play/pause (configurable)
- Lifecycle Management – Automatic ExoPlayer lifecycle handling
- Seek Tracking – Callbacks for seek start and end events
- Time Updates – Continuous time updates during playback (similar to HTML5
onTimeUpdate) - Volume Control – Complete volume management with mute/unmute, volume level control, and device volume monitoring
- AutoPlay – Automatic playback start when media is ready (configurable)
- Loop Playback – Seamless looping functionality for continuous playback
- Playback Rate Control – Adjustable playback speed from 0.25x to 2.0x with multiple speed options
- Android Studio Arctic Fox or newer
- Android SDK version 24 (Android 7.0) or higher
- Kotlin 1.8 or higher
- AndroidX Media3 1.9.0
Add the following to your build.gradle.kts (or build.gradle):
dependencies {
implementation("io.fastpix.player:android-player-sdk:1.0.3")
}Or if using version catalogs, add to libs.versions.toml:
[versions]
fastpix-player = "1.0.2"
[libraries]
fastpix-player = { module = "io.fastpix.player:android-player-sdk", version.ref = "fastpix-player" }Sync your project to download the dependency.
<io.fastpix.media3.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />Important: Assign an android:id to enable configuration change survival. Without an ID, a new player will be created on each configuration change.
import io.fastpix.media3.FastPixPlayer
import io.fastpix.media3.PlayerView
import io.fastpix.media3.PlaybackListener
import io.fastpix.media3.core.PlaybackResolution
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var fastPixPlayer: FastPixPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupPlayer()
}
private fun setupPlayer() {
// Create FastPixPlayer with configuration using builder pattern
fastPixPlayer = FastPixPlayer.Builder(this)
.setLoop(false) // Enable looping (optional)
.setAutoplay(true) // Enable autoplay (optional)
.build()
// Pass the configured player to PlayerView
binding.playerView.player = fastPixPlayer
// Set FastPix media item using builder pattern
fastPixPlayer.setFastPixMediaItem {
playbackId = "your-playback-id"
maxResolution = PlaybackResolution.FHD_1080
}
// Add playback listener
fastPixPlayer.addPlaybackListener(object : PlaybackListener {
override fun onPlay() {
// Playback started
}
override fun onPause() {
// Playback paused
}
override fun onTimeUpdate(
currentPositionMs: Long,
durationMs: Long,
bufferedPositionMs: Long
) {
// Update UI with current time, duration, and buffered position
}
override fun onError(error: PlaybackException) {
// Handle playback error
}
override fun onVolumeChanged(volumeLevel: Float) {
// Handle volume changes from device buttons
}
override fun onPlaybackRateChanged(rate: Float) {
// Handle playback speed changes
}
})
// Autoplay is already configured, no need to call play() if autoplay is enabled
}
override fun onDestroy() {
super.onDestroy()
fastPixPlayer.removePlaybackListener(playbackListener)
if (isFinishing) {
binding.playerView.release()
}
}
}import io.fastpix.media3.PlayerView
import io.fastpix.media3.PlaybackListener
import androidx.media3.common.MediaItem
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupPlayer()
}
private fun setupPlayer() {
// Set media item with direct URL
val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
binding.playerView.setMediaItem(mediaItem)
// Add playback listener
binding.playerView.addPlaybackListener(object : PlaybackListener {
override fun onPlay() {
// Playback started
}
override fun onPause() {
// Playback paused
}
override fun onTimeUpdate(
currentPositionMs: Long,
durationMs: Long,
bufferedPositionMs: Long
) {
// Update UI with current time, duration, and buffered position
}
override fun onError(error: PlaybackException) {
// Handle playback error
}
})
// Start playback
binding.playerView.play()
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
binding.playerView.release()
}
}
}The main view component that wraps ExoPlayer and provides a clean API.
// Set a single media item with direct URL
playerView.setMediaItem(MediaItem.fromUri("https://example.com/video.mp4"))
// Set a FastPix media item using builder pattern (recommended for FastPix streams)
playerView.setFastPixMediaItem {
playbackId = "your-playback-id"
maxResolution = PlaybackResolution.FHD_1080
playbackToken = "your-token" // Optional, for secure playback
}// Playback control
playerView.play() // Start or resume playback
playerView.pause() // Pause playback
playerView.togglePlayPause() // Toggle between play and pause
val isPlaying = playerView.isPlaying() // Check if currently playing
// Seek control
playerView.seekTo(positionMs = 5000) // Seek to 5 seconds
// Playback state
val currentPosition = playerView.getCurrentPosition() // Current position in ms
val duration = playerView.getDuration() // Total duration in ms
val playbackState = playerView.getPlaybackState() // Player state constant
// Volume control
playerView.setVolume(0.5f) // Set volume (0.0f = muted, 1.0f = max)
val volume = playerView.getVolume() // Get current volume level
playerView.mute() // Mute playback (saves volume for restoration)
playerView.unmute() // Restore previous volume level
// Playback speed control
playerView.setPlaybackSpeed(1.5f) // Set playback speed (e.g., 1.5x)
val speed = playerView.getPlaybackSpeed() // Get current playback speed
val availableSpeeds = playerView.getAvailablePlaybackSpeeds() // Get all available speeds// Enable/disable configuration change survival (default: true)
playerView.retainPlayerOnConfigChange = true
// Enable/disable tap gesture for play/pause (default: true)
playerView.isTapGestureEnabled = true
// Set whether playback should start automatically when ready
playerView.setPlayWhenReady(true)
val playWhenReady = playerView.getPlayWhenReady()
// Configure loop and autoplay (using FastPixPlayer.Builder)
val player = FastPixPlayer.Builder(context)
.setLoop(true) // Enable looping
.setAutoplay(true) // Enable autoplay
.build()
playerView.player = player
// Or configure at runtime
player.loop = true
player.autoplay = true// Add playback listener
playerView.addPlaybackListener(playbackListener)
// Remove playback listener
playerView.removePlaybackListener(playbackListener)
// Clear all listeners
playerView.clearPlaybackListeners()// Get underlying ExoPlayer instance for advanced usage
val exoPlayer = playerView.getPlayer()The SDK provides a builder pattern for creating FastPix media items with advanced configuration options. This is the recommended way to play FastPix streams.
import io.fastpix.media3.core.PlaybackResolution
// Simple usage with just playback ID
playerView.setFastPixMediaItem {
playbackId = "your-playback-id"
}playerView.setFastPixMediaItem {
playbackId = "your-playback-id"
// Resolution options
maxResolution = PlaybackResolution.FHD_1080 // Maximum resolution
minResolution = PlaybackResolution.HD_720 // Minimum resolution
resolution = PlaybackResolution.FHD_1080 // Fixed resolution
// Adaptive streaming
renditionOrder = RenditionOrder.Descending // Quality preference order
// Custom domain (defaults to "stream.fastpix.io")
customDomain = "custom.stream.fastpix.io"
// Stream type
streamType = "on-demand" // or "live-stream"
// Secure playback
playbackToken = "your-playback-token"
}enum class PlaybackResolution {
LD_480, // 480p
LD_540, // 540p
HD_720, // 720p
FHD_1080, // 1080p
QHD_1440, // 1440p
FOUR_K_2160 // 2160p (4K)
}Controls the order of preference for adaptive streaming:
enum class RenditionOrder {
Descending, // Prefer higher quality first
Ascending, // Prefer lower quality first
Default // Use default order
}class VideoPlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoPlayerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
setupPlayer()
}
private fun setupPlayer() {
// Configure FastPix media item with builder
val success = binding.playerView.setFastPixMediaItem {
playbackId = "your-playback-id"
maxResolution = PlaybackResolution.FHD_1080
playbackToken = "your-token" // Optional
}
if (!success) {
// Handle error (e.g., invalid playback ID)
Toast.makeText(this, "Failed to load video", Toast.LENGTH_SHORT).show()
return
}
// Add playback listener
binding.playerView.addPlaybackListener(object : PlaybackListener {
override fun onPlay() {
// Playback started
}
override fun onError(error: PlaybackException) {
// Handle playback error
}
})
// Start playback
binding.playerView.setPlayWhenReady(true)
}
}The setFastPixMediaItem method returns true if the media item was successfully set, or false if there was an error. Errors are automatically reported through the PlaybackListener.onError() callback.
Common errors:
- Empty playback ID
- Invalid stream type (must be "on-demand" or "live-stream")
- Invalid playback token (if provided)
Interface for receiving playback events and time updates.
interface PlaybackListener {
fun onPlay() // Called when playback starts/resumes
fun onPause() // Called when playback is paused
fun onPlaybackStateChanged(isPlaying: Boolean) // Called when play/pause state changes
fun onError(error: PlaybackException) // Called when a playback error occurs
// Time updates (called periodically during playback)
fun onTimeUpdate(
currentPositionMs: Long,
durationMs: Long,
bufferedPositionMs: Long
)
// Seek callbacks
fun onSeekStart(currentPositionMs: Long) // Called when seek starts
fun onSeekEnd(
fromPositionMs: Long,
toPositionMs: Long,
durationMs: Long
) // Called when seek completes
// Buffering callbacks
fun onBufferingStart() // Called when buffering starts
fun onBufferingEnd() // Called when buffering ends
// Volume callbacks
fun onVolumeChanged(volumeLevel: Float) // Called when device volume changes
fun onMuteStateChanged(isMuted: Boolean) // Called when mute state changes
// Playback rate callback
fun onPlaybackRateChanged(rate: Float) // Called when playback speed changes
// Completion callback
fun onCompleted() // Called when video playback completes (reaches the end)
}val listener = object : PlaybackListener {
override fun onPlay() {
// Update play button UI
}
override fun onPause() {
// Update pause button UI
}
override fun onTimeUpdate(
currentPositionMs: Long,
durationMs: Long,
bufferedPositionMs: Long
) {
// Update seek bar and time displays
seekBar.progress = currentPositionMs.toInt()
seekBar.max = durationMs.toInt()
seekBar.secondaryProgress = bufferedPositionMs.toInt()
currentTimeTextView.text = formatTime(currentPositionMs)
durationTextView.text = formatTime(durationMs)
}
override fun onError(error: PlaybackException) {
// Show error message to user
Toast.makeText(context, "Playback error: ${error.message}", Toast.LENGTH_LONG).show()
}
override fun onSeekStart(currentPositionMs: Long) {
// Pause time updates UI or show seeking indicator
}
override fun onSeekEnd(
fromPositionMs: Long,
toPositionMs: Long,
durationMs: Long
) {
// Resume time updates UI or hide seeking indicator
}
override fun onBufferingStart() {
// Show buffering indicator
}
override fun onBufferingEnd() {
// Hide buffering indicator
}
override fun onVolumeChanged(volumeLevel: Float) {
// Update volume UI when device volume changes
volumeSlider.progress = (volumeLevel * 100).toInt()
}
override fun onMuteStateChanged(isMuted: Boolean) {
// Update mute icon
muteButton.setImageResource(if (isMuted) R.drawable.ic_volume_off else R.drawable.ic_volume_on)
}
override fun onPlaybackRateChanged(rate: Float) {
// Update playback speed UI
speedButton.text = "${rate}x"
}
}
playerView.addPlaybackListener(listener)FastPix Player SDK provides comprehensive volume control with support for programmatic volume adjustment, mute/unmute functionality, and automatic device volume monitoring.
// Set volume level (0.0f = muted, 1.0f = maximum)
playerView.setVolume(0.75f)
// Get current volume level
val currentVolume = playerView.getVolume()
// Mute playback (saves current volume for restoration)
playerView.mute()
// Unmute and restore previous volume
playerView.unmute()The SDK automatically monitors device volume changes (via hardware buttons or system controls) and notifies listeners:
playerView.addPlaybackListener(object : PlaybackListener {
override fun onVolumeChanged(volumeLevel: Float) {
// Called when device volume changes
// volumeLevel is between 0.0f (muted) and 1.0f (maximum)
updateVolumeUI(volumeLevel)
}
override fun onMuteStateChanged(isMuted: Boolean) {
// Called when mute state changes
updateMuteIcon(isMuted)
}
})- Volume Range: 0.0f (muted) to 1.0f (maximum volume)
- Mute/Unmute: Smart mute that saves volume level for restoration
- Device Volume Monitoring: Automatic detection of hardware volume button changes
- State Preservation: Volume state is preserved across configuration changes
AutoPlay allows playback to start automatically when the media is ready, without requiring a manual call to play().
AutoPlay can be configured during player creation using the builder pattern:
val player = FastPixPlayer.Builder(context)
.setAutoplay(true) // Enable autoplay
.build()
playerView.player = playerYou can also enable or disable autoplay at runtime:
// Enable autoplay
player.autoplay = true
// Disable autoplay
player.autoplay = false
// Check current autoplay state
val isAutoplayEnabled = player.autoplay- When
autoplay = true: Playback automatically starts when media is ready - When
autoplay = false: Playback must be started manually viaplay()orsetPlayWhenReady(true) - Autoplay state is preserved across configuration changes
Loop playback enables the video to automatically restart from the beginning when it reaches the end, creating a seamless continuous playback experience.
Loop can be configured during player creation using the builder pattern:
val player = FastPixPlayer.Builder(context)
.setLoop(true) // Enable looping
.build()
playerView.player = playerYou can also enable or disable looping at runtime:
// Enable looping
player.loop = true
// Disable looping
player.loop = false
// Check current loop state
val isLooping = player.loop- When
loop = true: Playback automatically restarts from the beginning when it reaches the end - When
loop = false: Playback stops when it reaches the end - Loop state is preserved across configuration changes
- The
onCompleted()callback is still triggered when the video reaches the end, even with looping enabled
Playback rate control allows users to adjust the playback speed from 0.25x (slow motion) to 2.0x (double speed), providing flexibility for different viewing preferences.
The SDK supports the following playback speeds:
- 0.25x - Quarter speed (slow motion)
- 0.5x - Half speed
- 0.75x - Three-quarter speed
- 1.0x - Normal speed (default)
- 1.25x - 1.25x speed
- 1.5x - 1.5x speed
- 1.75x - 1.75x speed
- 2.0x - Double speed
// Set playback speed to 1.5x
playerView.setPlaybackSpeed(1.5f)
// Get current playback speed
val currentSpeed = playerView.getPlaybackSpeed()
// Get all available playback speeds
val availableSpeeds = playerView.getAvailablePlaybackSpeeds()
// Returns: [0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f]Listen for playback speed changes:
playerView.addPlaybackListener(object : PlaybackListener {
override fun onPlaybackRateChanged(rate: Float) {
// Called when playback speed changes
// rate is the new playback speed (e.g., 1.5f for 1.5x)
updateSpeedUI(rate)
}
})private fun showPlaybackSpeedMenu() {
val popupMenu = PopupMenu(this, speedButton)
val availableSpeeds = playerView.getAvailablePlaybackSpeeds()
val currentSpeed = playerView.getPlaybackSpeed()
availableSpeeds.forEachIndexed { index, speed ->
val speedLabel = if (speed == speed.toInt().toFloat()) {
"${speed.toInt()}x"
} else {
String.format("%.2fx", speed).trimEnd('0').trimEnd('.')
}
val menuItem = popupMenu.menu.add(0, index, 0, speedLabel)
if (kotlin.math.abs(speed - currentSpeed) < 0.01f) {
menuItem.isChecked = true
}
}
popupMenu.menu.setGroupCheckable(0, true, true)
popupMenu.setOnMenuItemClickListener { item ->
val selectedSpeed = availableSpeeds[item.itemId]
playerView.setPlaybackSpeed(selectedSpeed)
true
}
popupMenu.show()
}- Automatic Speed Adjustment: If an exact speed is not available, the SDK automatically selects the closest available speed
- State Preservation: Playback speed is preserved across configuration changes
- Pitch Preservation: Audio pitch remains normal at all speeds (no chipmunk effect)
By default, PlayerView preserves playback state across configuration changes (rotation, multi-window, etc.). This means:
- ✅ Video playback does NOT restart on rotation
- ✅ Current playback position is preserved
- ✅ Play/pause state is preserved
- ✅ Buffering state is preserved
The player instance is retained in an internal registry when the view is detached during configuration changes, and reattached to the same instance when the view is recreated.
If you need to disable this behavior:
playerView.retainPlayerOnConfigChange = falseWhen disabled:
- Player is released when view is detached
- A fresh player instance is created on reattach
- Playback will restart from the beginning
PlayerView supports fullscreen mode where the player covers the entire screen. When entering fullscreen:
- PlayerView is detached from its original parent
- Attached to the Activity's root decor view
- System UI (status bar, navigation bar) is hidden
- Playback state and listeners are preserved
When exiting fullscreen:
- PlayerView is restored to its original parent
- System UI is restored
- Playback continues seamlessly
- Fullscreen is developer-controlled, not automatic
- Fullscreen state is automatically cleaned up if the view is detached
- Supports both portrait and landscape orientations
- Does NOT force orientation changes
- Handles orientation changes while in fullscreen
PlayerView automatically handles ExoPlayer lifecycle:
- Creates player when attached to window
- Preserves player instance across configuration changes (when
retainPlayerOnConfigChangeis true) - Releases player when view is truly destroyed (not during config changes)
Call release() when the Activity is finishing:
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
playerView.release()
}
}This SDK:
- ✅ Does NOT use
android:configChanges(follows Android best practices) - ✅ Does NOT require Activity or Fragment lifecycle ownership
- ✅ Does NOT require ViewModel usage
- ✅ Does NOT leak Activity or View references
- ✅ Uses an internal player registry to retain instances across view recreation
See the app module for a complete example implementation demonstrating:
- Basic playback control
- Playback event listeners
- Seek bar integration
- Fullscreen mode
- Configuration change handling