🚀 A modern, clean Android DLNA/UPnP casting library designed as a drop-in replacement for the discontinued Cling project.
中文文档 | English Documentation
🎯 Enhanced Volume Control & Millisecond-Level Progress Management
- 🔊 Complete Volume Control System: Added
getVolume(),setVolume(), andsetMute()APIs for comprehensive volume management - ⚡ Millisecond-Level Progress Control: Intelligent caching with 3-second cache duration and real-time interpolation
- 🚀 Smart Cache Management: Volume cache (5-second validity) and progress cache with async refresh mechanisms
- 🎯 Real-time Progress Tracking:
getProgressRealtime()for force refresh without cache dependency - 🔄 Manual Cache Control: Exposed cache refresh and clearing methods for advanced control
- 📊 Enhanced State Management: Improved
getState()with integrated volume and mute status
- 🔍 Device Discovery: Automatic DLNA/UPnP device discovery with SSDP protocol
- 📺 Media Casting: Cast photos, videos, and audio to DLNA-compatible devices
- 🎮 Playback Controls: Play, pause, stop, seek, volume control, and mute functionality
- 🔊 Advanced Volume Control: Get/set volume, mute control with intelligent caching
- ⚡ Millisecond Precision: Real-time progress tracking with smart interpolation
- 📱 Easy Integration: Simple API with intuitive callback mechanisms
- 🚀 Modern Architecture: Built with Kotlin, Coroutines, and Android best practices
- 🔧 Highly Compatible: Tested with major TV brands (Xiaomi, Samsung, LG, Sony)
- ⚡ Lightweight: Minimal dependencies, optimized performance
Add to your app's build.gradle:
dependencies {
implementation 'com.yinnho.upnpcast:upnpcast:1.1.2'
}Add to your root build.gradle:
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}Add dependency:
dependencies {
implementation 'com.github.yinnho:UPnPCast:1.1.2'
}import com.yinnho.upnpcast.DLNACast
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize
DLNACast.init(this)
// Use coroutines for all operations
lifecycleScope.launch {
searchDevices()
performSmartCast()
}
}
private suspend fun searchDevices() {
try {
// Device discovery with timeout
val devices = DLNACast.search(timeout = 5000)
Log.d("DLNA", "Found ${devices.size} devices")
// Display devices
devices.forEach { device ->
val icon = if (device.isTV) "📺" else "📱"
Log.d("DLNA", "$icon ${device.name} (${device.address})")
}
} catch (e: Exception) {
Log.e("DLNA", "Search failed: ${e.message}")
}
}
private suspend fun performSmartCast() {
try {
// Smart cast - automatically finds and selects best device
val success = DLNACast.cast("http://your-video.mp4", "Video Title")
if (success) {
Log.d("DLNA", "Smart casting started!")
controlPlayback()
} else {
Log.e("DLNA", "Cast failed")
}
} catch (e: Exception) {
Log.e("DLNA", "Cast error: ${e.message}")
}
}
private suspend fun controlPlayback() {
try {
// Control playback
val pauseSuccess = DLNACast.control(DLNACast.MediaAction.PAUSE)
Log.d("DLNA", "Paused: $pauseSuccess")
// Get current state
val state = DLNACast.getState()
Log.d("DLNA", "Connected: ${state.isConnected}, Playing: ${state.isPlaying}")
// Seek to 30 seconds
val seekSuccess = DLNACast.seek(30000)
Log.d("DLNA", "Seeked to 30 seconds: $seekSuccess")
} catch (e: Exception) {
Log.e("DLNA", "Control error: ${e.message}")
}
}
override fun onDestroy() {
super.onDestroy()
DLNACast.cleanup()
}
}// Initialize the library (call once in onCreate)
DLNACast.init(context: Context)
// Search for devices (returns list of discovered devices)
suspend fun DLNACast.search(timeout: Long = 5000): List<Device>
// Smart cast - automatically selects best available device
suspend fun DLNACast.cast(url: String, title: String? = null): Boolean
// Cast to specific device
suspend fun DLNACast.castToDevice(device: Device, url: String, title: String): Boolean
// Cast local video files
suspend fun DLNACast.castLocalFile(device: Device, video: LocalVideo): Boolean
// Scan for local video files
suspend fun DLNACast.scanLocalVideos(): List<LocalVideo>
// Media control operations
suspend fun DLNACast.control(action: MediaAction): Boolean
// Seek to specific position (in milliseconds)
suspend fun DLNACast.seek(positionMs: Long): Boolean// Get current casting state (synchronous)
fun DLNACast.getState(): State
// Get playback progress (synchronous)
fun DLNACast.getProgress(): Progress
// Get volume information (synchronous)
fun DLNACast.getVolume(): Volume
// Clean up resources (call in onDestroy)
fun DLNACast.cleanup()// Device information
data class Device(
val id: String, // Unique device identifier
val name: String, // Display name (e.g., "Living Room TV")
val address: String, // IP address
val isTV: Boolean // Whether this is a TV device
)
// Local video file information
data class LocalVideo(
val path: String, // Full file path
val name: String, // Display name
val size: Long, // File size in bytes
val duration: Long // Duration in milliseconds
)
// Media control actions
enum class MediaAction {
PLAY, PAUSE, STOP
}
// Playback states
enum class PlaybackState {
IDLE, // Not connected or no media
PLAYING, // Currently playing
PAUSED, // Playback paused
STOPPED, // Playback stopped
BUFFERING, // Loading/buffering
ERROR // Error state
}
// Current casting state
data class State(
val isConnected: Boolean, // Connected to a device
val currentDevice: Device?, // Current target device
val playbackState: PlaybackState, // Current playback state
val isPlaying: Boolean, // Whether media is playing
val isPaused: Boolean, // Whether media is paused
val volume: Int, // Current volume (0-100)
val isMuted: Boolean // Whether audio is muted
)
// Playback progress information
data class Progress(
val currentMs: Long, // Current position in milliseconds
val totalMs: Long, // Total duration in milliseconds
val percentage: Float // Progress as percentage (0.0-1.0)
)
// Volume information
data class Volume(
val level: Int, // Volume level (0-100)
val isMuted: Boolean // Mute status
)lifecycleScope.launch {
try {
// First, search for devices
val devices = DLNACast.search(timeout = 5000)
// Find your preferred device
val targetDevice = devices.firstOrNull { it.name.contains("Living Room") }
if (targetDevice != null) {
// Cast to specific device
val success = DLNACast.castToDevice(
device = targetDevice,
url = "http://your-video.mp4",
title = "My Movie"
)
if (success) {
Log.d("DLNA", "Successfully cast to ${targetDevice.name}")
}
}
} catch (e: Exception) {
Log.e("DLNA", "Cast failed: ${e.message}")
}
}lifecycleScope.launch {
try {
// Scan for local video files
val localVideos = DLNACast.scanLocalVideos()
// Find a video to cast
val videoToPlay = localVideos.firstOrNull { it.name.contains("movie") }
if (videoToPlay != null) {
// Get available devices
val devices = DLNACast.search()
val device = devices.firstOrNull()
if (device != null) {
// Cast local file
val success = DLNACast.castLocalFile(device, videoToPlay)
Log.d("DLNA", "Local cast success: $success")
}
}
} catch (e: Exception) {
Log.e("DLNA", "Local cast failed: ${e.message}")
}
}lifecycleScope.launch {
try {
// Control playback
DLNACast.control(DLNACast.MediaAction.PAUSE)
// Monitor state
val state = DLNACast.getState()
Log.d("DLNA", "Device: ${state.currentDevice?.name}")
Log.d("DLNA", "Playing: ${state.isPlaying}")
Log.d("DLNA", "Volume: ${state.volume}")
// Get progress
val progress = DLNACast.getProgress()
Log.d("DLNA", "Progress: ${progress.percentage * 100}%")
// Seek to specific position (2 minutes)
DLNACast.seek(120000)
} catch (e: Exception) {
Log.e("DLNA", "Control failed: ${e.message}")
}
}- 🎯 Demo App - Working example application with complete API demonstration
- 📖 API Reference - Complete API documentation above
- 📋 Changelog - Version history and updates
- 🤔 FAQ - Frequently asked questions and troubleshooting
- 🎯 Best Practices - Async callbacks, device management, and optimization guides
- ✅ Xiaomi TV (Native DLNA + Mi Cast)
- ✅ Samsung Smart TV
- ✅ LG Smart TV
- ✅ Sony Bravia TV
- ✅ Android TV boxes
- ✅ Windows Media Player
This project is licensed under the MIT License - see the LICENSE file for details.
We welcome contributions! Please see our Contributing Guide for development guidelines and how to get started.
- 📖 Detailed usage examples in the demo app
- 🐛 Report issues on GitHub Issues
- 💡 Feature requests are welcome!
// Cast media to specific device DLNACast.castToDevice(device, "http://example.com/video.mp4", "My Video") { success -> // Handle result }
// Cast local files DLNACast.castLocalFile("/storage/emulated/0/video.mp4", "Local Video") { success, message -> if (success) { println("Local file cast successful") } else { println("Cast failed: $message") } }
// Get local file URL for manual use
val fileUrl = DLNACast.getLocalFileUrl("/storage/emulated/0/video.mp4")
if (fileUrl != null) {
DLNACast.cast(fileUrl, "My Local Video") { success ->
// Handle result
}
}
// Control media playback