A libVLC Wrapper for H.265 IoT Live Streams on Android
Simple Encapsulation of an Android Live Video Player
EasyVlcPlayer Utility Class Analysis and Documentation
An Android video player encapsulation based on
libVLC, specifically designed for playing RTSP/RTMP/HTTPS live video streams in IoT scenarios.
1. Dependency Import:
implementation 'org.videolan.android:libvlc-all:3.6.5'
2. EasyVlcPlayer Complete Code:
package com.xx.xx.iot.webrtc
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.TextureView
import androidx.core.net.toUri
import org.videolan.libvlc.LibVLC
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.util.VLCVideoLayout
/**
* @author: smile
* @time: 2025/11/18 19:02
* @description: vlc player
**/
class EasyVlcPlayer(private val context: Context) {
// VLC core components
private var libVLC: LibVLC ? =null
private var mediaPlayer: MediaPlayer ?= null
// Playback state callback
private var listener: OnPlayListener? = null
private val progressHandler = Handler(Looper.getMainLooper())
private var progressRunnable: Runnable? = null // Progress update task
private var isInitialized = false
private val TAG = EasyVlcPlayer::class.java.name
private var surface: VLCVideoLayout? = null
// Playback state listener interface
interface OnPlayListener {
fun onPrepared() // Preparation complete
fun onError(message: String) // Error callback
fun onStopped() // Stop callback
fun onProgress(currentPosition: Long, totalDuration: Long)
}
fun setOnPlayListener(listener: OnPlayListener) {
this.listener = listener
}
fun isInitialized(): Boolean = isInitialized
// Initialize VLC
fun init() = try {
// Configure VLC parameters
val options = mutableListOf(
"--no-video-title-show",//Hide video title bar
"--network-caching=800",//Network buffer time
"--demux=hevc",//Force H.265 demuxing
"--codec=hevc",//Force H.265 decoding
"--http-reconnect",//Enable HTTP auto-reconnect on disconnect
"--vout=android_display",//Specify video output module as Android native display module
"--live-caching=800",//Set live stream specific cache time
"-vvv",// Keep logs for easy debugging (remove when publishing)
"--hevc-fps=20",//Set frame rate to 20fps
"--avcodec-skiploopfilter=all", // Reduce decoder pressure
"--android-display-chroma=RV32", // Specify color format
"--avcodec-hw=none", // Disable hardware decoding
"--video-on-top",
)
libVLC = LibVLC(context, options)
mediaPlayer = MediaPlayer(libVLC)
mediaPlayer?.spuTrack = -1 // Disable subtitles
isInitialized = true
} catch (e: Exception) {
listener?.onError("VLC initialization failed: ${e.message}")
isInitialized = false
}
// Set playback container
fun setSurface(vlcVideoLayout: VLCVideoLayout) {
if (vlcVideoLayout.width == 0 || vlcVideoLayout.height == 0) {
Log.d(TAG, "Surface not ready (width/height is 0), delaying binding")
vlcVideoLayout.post {
this.surface = vlcVideoLayout
Log.d(TAG, "Surface delayed binding complete")
}
return
}
this.surface = vlcVideoLayout
mediaPlayer?.attachViews(vlcVideoLayout, null, false, false)
}
// Play RTSP/RTMP stream
fun play(streamUrl: String) {
try {
if (!isInitialized) {
listener?.onError("Player not initialized")
return
}
if (surface == null) {
listener?.onError("Playback control not bound")
return
}
try {
val uri = streamUrl.toUri()
.buildUpon()
.scheme("https") // Force HTTPS protocol
.build()
val media = Media(libVLC, uri)
mediaPlayer?.let {
it.media = media
media.release()
it.play()
it.setEventListener { event ->
when (event.type) {
MediaPlayer.Event.Playing -> Log.d(TAG,"Preparation complete")
MediaPlayer.Event.EncounteredError -> listener?.onError("Playback error: ${it.media?.uri}")
MediaPlayer.Event.EndReached,
MediaPlayer.Event.Stopped -> {
stop()
listener?.onStopped()
}
}
}
}
} catch (e: Exception) {
listener?.onError("Playback failed: ${e.message}")
}
} catch (e: Exception) {
listener?.onError("Playback failed: ${e.message}")
}
}
fun seekTo(position: Long) {
mediaPlayer?.time = position
}
fun getTotalDuration(): Long {
return mediaPlayer?.length ?: 0L
}
private fun stopProgressUpdate() {
progressRunnable?.let { progressHandler.removeCallbacks(it) }
}
/**
* Pause rendering
*/
fun pauseRender() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause() // Pause decoding first
}
it.detachViews()
stopProgressUpdate()
Log.d(TAG, "Rendering paused, BufferQueue released")
}
}
/**
* Resume rendering (call when switching to foreground/unlocking: rebuild rendering pipeline)
*/
fun resumeRender() {
val currentSurface = surface ?: run {
listener?.onError("Resume rendering failed: Playback control not bound")
return
}
if (currentSurface.width > 0 && currentSurface.height > 0) {
mediaPlayer?.let { player ->
player.attachViews(currentSurface, null, false, false) // Re-bind Surface
if (!player.isPlaying) {
player.play()
}
Log.d(TAG, "Rendering resumed, BufferQueue rebuilt")
}
} else {
currentSurface.postDelayed({
resumeRender()
}, 300)
Log.w(TAG, "Surface not ready, delaying rendering resume")
}
}
// Pause playback
fun pause() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause()
stopProgressUpdate()
}
}
}
// Resume playback
fun resume() {
if (mediaPlayer == null || !isInitialized) {
listener?.onError("Player not initialized")
return
}
val currentSurface = surface ?: run {
listener?.onError("Playback control not bound")
return
}
if (currentSurface.width > 0 && currentSurface.height > 0) {
mediaPlayer?.let { player ->
if (!player.isPlaying) {
player.detachViews()
player.attachViews(currentSurface, null, false, false)
player.play() // Resume playback
Log.d(TAG, "Playback resumed successfully, Surface dimensions: ${currentSurface.width}x${currentSurface.height}")
}
}
} else {
currentSurface.postDelayed({
resume()
}, 300)
Log.w(TAG, "Surface not ready (dimensions: ${currentSurface.width}x${currentSurface.height}), delaying playback resume")
}
}
// Stop playback
fun stop() {
mediaPlayer?.stop()
stopProgressUpdate()
}
// Release resources
fun release() {
stopProgressUpdate()
progressHandler.removeCallbacksAndMessages(null)
mediaPlayer?.stop()
mediaPlayer?.release()
libVLC?.release()
libVLC = null
mediaPlayer = null
surface = null
listener = null
isInitialized = false
}
fun detachSurface() {
mediaPlayer?.detachViews()
}
}
3. Class Overview
| Item | Description |
|---|---|
| Package Path | com.x.x.iot.webrtc |
| Author | smile |
| Core Dependency | org.videolan.libvlc (VLC Android SDK) |
| Main Purpose | Play real-time video streams pushed by devices (H.265/HEVC encoding) |
| Typical Scenario | Remote video monitoring for robots/IoT devices, live playback |
Design Positioning
EasyVlcPlayer is a lightweight encapsulation of libVLC, focusing on:
- Live Stream Playback: Supports RTSP, RTMP, HTTPS, and other protocols
- H.265 Hardware Decoding Adaptation: Forces HEVC decoding to adapt to IoT device encoding formats
- Foreground/Background Lifecycle Management: Handles Surface binding/unbinding via
pauseRender()/resumeRender() - Low-Latency Playback: Cache configured to 800ms, frame rate limited to 20fps
4. Core Members
4.1 Properties
| Property | Type | Description |
|---|---|---|
libVLC |
LibVLC? |
VLC core engine instance |
mediaPlayer |
MediaPlayer? |
VLC media player instance |
surface |
VLCVideoLayout? |
Video rendering container (Android Surface) |
listener |
OnPlayListener? |
Playback state callback listener |
progressHandler |
Handler |
Main thread Handler for progress updates |
progressRunnable |
Runnable? |
Progress update scheduled task |
isInitialized |
Boolean |
Initialization state flag |
4.2 Callback Interface
interface OnPlayListener {
fun onPrepared() // Preparation complete
fun onError(message: String) // Error callback
fun onStopped() // Stop callback
fun onProgress(currentPosition: Long, // Progress callback
totalDuration: Long)
}
5. Method Details
5.1 Initialization Process
fun init()
Execution Steps:
1. Configure VLC startup parameters (MutableList<String>)
├── --no-video-title-show → Hide video title bar
├── --network-caching=800 → Network buffer 800ms
├── --demux=hevc → Force H.265 demuxing
├── --codec=hevc → Force H.265 decoding
├── --http-reconnect → HTTP auto-reconnect on disconnect
├── --vout=android_display → Android native display module
├── --live-caching=800 → Live stream cache 800ms
├── -vvv → Verbose logs (remove when publishing)
├── --hevc-fps=20 → Limit frame rate to 20fps
├── --avcodec-skiploopfilter=all → Skip loop filter, reduce decoding pressure
├── --android-display-chroma=RV32 → Color format RV32
├── --avcodec-hw=none → Disable hardware decoding (compatibility priority)
└── --video-on-top → Video window on top
2. libVLC = LibVLC(context, options)
3. mediaPlayer = MediaPlayer(libVLC)
4. mediaPlayer?.spuTrack = -1 → Disable subtitle track
5. isInitialized = true
Exception Handling:
- Catches all exceptions, callback
listener?.onError("VLC initialization failed: ${e.message}") - Sets
isInitialized = false
5.2 Surface Binding
fun setSurface(vlcVideoLayout: VLCVideoLayout)
Logic Branches:
| Condition | Handling |
|---|---|
width == 0 || height == 0 |
Surface not ready, delay binding via post { } |
width > 0 && height > 0 |
Bind immediately: mediaPlayer?.attachViews(vlcVideoLayout, null, false, false) |
Note:
attachViews()must be called when the Surface is valid, otherwise rendering will fail.
5.3 Play Video Stream
fun play(streamUrl: String)
Complete Flow:
1. Pre-checks
├── !isInitialized → onError("Player not initialized") → return
└── surface == null → onError("Playback control not bound") → return
2. URL Processing (Key!)
└── streamUrl.toUri()
.buildUpon()
.scheme("https") // Force protocol to HTTPS
.build()
3. Create Media Object
└── val media = Media(libVLC, uri)
4. Set and Play
├── mediaPlayer?.media = media
├── media.release() // Immediately release Media object (VLC has internal reference)
└── mediaPlayer?.play()
⚠️ Force HTTPS: Regardless of the incoming URL protocol, it will be forcibly changed to
https://. This is for unified secure transmission but may cause RTSP/RTMP streams to fail (need to confirm server support).
5.4 Rendering Control (Foreground/Background Switching)
pauseRender() — Pause Rendering (Switch to Background/Lock Screen)
fun pauseRender()
Execution Steps:
1. if (isPlaying) → pause() // Pause decoding
2. detachViews() // Unbind Surface, release BufferQueue
3. stopProgressUpdate() // Stop progress update task
Purpose: Called when the Activity goes into the background or the screen is locked, releasing Surface resources to avoid system errors.
resumeRender() — Resume Rendering (Switch to Foreground/Unlock)
fun resumeRender()
Execution Steps:
1. Check surface validity
└── surface == null → onError("Resume rendering failed: Playback control not bound") → return
2. Check Surface dimensions
├── width > 0 && height > 0
│ ├── attachViews(currentSurface, null, false, false) // Rebuild BufferQueue
│ └── if (!isPlaying) → play()
│
└── width == 0 || height == 0
└── postDelayed(300ms) → resumeRender() // Recursive delayed wait
Purpose: Rebuild the rendering pipeline when the Activity returns to the foreground to restore video display.
5.5 Playback Control
| Method | Function | Internal Logic |
|---|---|---|
pause() |
Pause playback | mediaPlayer?.pause() + stopProgressUpdate() |
resume() |
Resume playback | Check initialization/Surface → detachViews() → attachViews() → play() |
stop() |
Stop playback | mediaPlayer?.stop() + stopProgressUpdate() |
seekTo(position) |
Seek progress | mediaPlayer?.time = position |
getTotalDuration() |
Get total duration | mediaPlayer?.length ?: 0L |
detachSurface() |
Unbind Surface | mediaPlayer?.detachViews() |
5.6 Resource Release
fun release()
Release Order (Strict!):
1. stopProgressUpdate() // Stop progress task
2. progressHandler.removeCallbacksAndMessages(null) // Clear Handler message queue
3. mediaPlayer?.stop() // Stop playback
4. mediaPlayer?.release() // Release player
5. libVLC?.release() // Release VLC engine
6. libVLC = null // Nullify reference
7. mediaPlayer = null
8. surface = null
9. listener = null
10. isInitialized = false
⚠️ Important: Must be called in Activity
onDestroy(), otherwise it will cause memory leaks and native layer crashes.
6. VLC Parameter Configuration Details
| Parameter | Value | Function |
|---|---|---|
--no-video-title-show |
- | Hide video title bar to avoid blocking the picture |
--network-caching=800 |
800ms | Network stream buffer time, reduce latency |
--demux=hevc |
hevc | Force use of H.265 demuxer |
--codec=hevc |
hevc | Force use of H.265 decoder |
--http-reconnect |
- | Auto-reconnect after HTTP disconnect |
--vout=android_display |
android_display | Use Android native display output |
--live-caching=800 |
800ms | Live stream specific cache |
-vvv |
- | Highest level logs (for debugging, remove when publishing) |
--hevc-fps=20 |
20fps | Limit HEVC decoding frame rate, reduce CPU usage |
--avcodec-skiploopfilter=all |
all | Skip loop filter, reduce decoding computation |
--android-display-chroma=RV32 |
RV32 | Specify RGB32 color format |
--avcodec-hw=none |
none | Disable hardware decoding, use software decoding (compatibility priority) |
--video-on-top |
- | Video window on top |
7. Lifecycle Management
7.1 Activity Lifecycle Mapping
| Activity Callback | Method to Call | Description |
|---|---|---|
onCreate() |
EasyVlcPlayer(context) → init() |
Initialize player |
onResume() |
resumeRender() |
Resume rendering |
onPause() |
pauseRender() |
Pause rendering, release Surface |
onDestroy() |
release() |
Completely release resources |
7.2 State Transition Diagram
[Uninitialized]
│
▼ init()
[Initialized]
│
▼ setSurface()
[Surface Bound]
│
▼ play(url)
[Playing]
│
├──► pauseRender() ──► [Rendering Paused]
│ │
│ ▼ resumeRender()
│ [Rendering Resumed]
│
├──► pause() ──► [Paused]
│ │
│ ▼ resume()
│ [Playing]
│
└──► stop() / release()
│
▼
[Stopped/Released]
8. Notes and Potential Issues
8.1 ⚠️ Force HTTPS Issue
val uri = streamUrl.toUri()
.buildUpon()
.scheme("https") // Force HTTPS
.build()
Risk:
- If the incoming address is an
rtsp://orrtmp://stream, forcing it tohttps://will cause playback failure - It is recommended to dynamically set the scheme based on the actual protocol type, or ensure the server provides HTTPS streams simultaneously
8.2 ⚠️ Disabling Hardware Decoding
"--avcodec-hw=none"
Impact:
- Uses pure software decoding, higher CPU usage
- May cause stuttering and overheating on low-end devices
- Advantage is good compatibility, avoiding black/flashing screens caused by hardware decoding compatibility issues
8.3 ⚠️ Surface Not Ready Handling
setSurface() and resumeRender() both handle the width/height == 0 case:
vlcVideoLayout.post { this.surface = layout } // setSurface delayed binding
currentSurface.postDelayed({ resumeRender() }, 300) // resumeRender recursive wait
Suggestion: Ensure play() is called only after the Surface is fully created to avoid unnecessary delays.
8.4 ⚠️ Progress Update Not Implemented
The code defines progressRunnable and progressHandler, but does not actually start the progress update task. If a progress bar function is needed, add:
// Start after play() succeeds
progressRunnable = object : Runnable {
override fun run() {
val current = mediaPlayer?.time ?: 0L
val total = mediaPlayer?.length ?: 0L
listener?.onProgress(current, total)
progressHandler.postDelayed(this, 1000)
}
}
progressHandler.post(progressRunnable!!)
8.5 ⚠️ Event Listener Commented Out
/* it.setEventListener { event ->
when (event.type) {
MediaPlayer.Event.Playing -> ...
MediaPlayer.Event.EncounteredError -> ...
MediaPlayer.Event.EndReached -> ...
}
} */
Impact: Playback completion, errors, and other events cannot be notified to the upper layer through VLC internal event callbacks, and can only be perceived through external calls.
8.6 Video Playback Stuttering and Black Screen Issues
- Adjust network buffer format
- Enable auto-reconnect after disconnection
- Set live stream specific cache time
- Adjust decoding format based on device protocol
- Adjust frame rate based on requirements
- Adjust video stream data parsing timing, receive directly after TCP receives device message, dynamic parsing
- Retain the keyframe or last frame picture of the video
- Recycle various threads and resources when exiting the video live streaming interface
9. Usage Example
package com.x.x.iot.ui.activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.google.gson.Gson
import com.x.x.iot.R
import com.x.x.iot.databinding.ActivityVlcPlaerBinding
import com.x.x.iot.event.StreamMessageEvent
import com.x.x.iot.http.ApiException
import com.x.x.iot.http.ApiResult
import com.x.x.iot.http.EasyHttpUtil
import com.x.x.iot.http.weakHashMapOf
import com.x.x.iot.utils.IotConstants
import com.x.x.iot.utils.FlowBus
import com.x.x.iot.utils.LogUtils
import com.x.x.iot.webrtc.EasyVlcPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* @author: smile
* @time: 2025/11/18 18:31
* @description: Use vlc to play video stream
**/
class VlcVideoPlayerActivity : AppCompatActivity(), EasyVlcPlayer.OnPlayListener {
private val TAG = "VlcVideoPlayerActivity"
private lateinit var binding: ActivityVlcPlaerBinding
private lateinit var vlcPlayer: EasyVlcPlayer
private val mainHandler = Handler(Looper.getMainLooper())
private var videoUrl: String = ""
private var isPlaying = false
private var isPaused = false
private val liveScope = CoroutineScope(Dispatchers.IO)
private var liveJob: Job? = null
private var requestSuccess: Boolean = false
private var code: Int = 0
private var errorMsg = ""
private var deviceId = ""
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
binding = ActivityVlcPlaerBinding.inflate(layoutInflater)
setContentView(binding.root)
initVlcPlayer()
initData()
initViews()
setupClickListeners()
setUpEdgeToEdge()
}
private fun initVlcPlayer() {
//Keep screen on
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
vlcPlayer = EasyVlcPlayer(this)
vlcPlayer.setOnPlayListener(this)
vlcPlayer.init()
binding.vlcVideoLayout.post {
if (::vlcPlayer.isInitialized) {
vlcPlayer.setSurface(binding.vlcVideoLayout)
Log.d(TAG, "VLCVideoLayout dimensions: ${binding.vlcVideoLayout.width}x${binding.vlcVideoLayout.height}")
}
}
}
private fun initData() {
intent?.let {
deviceId = it.getStringExtra("deviceId").toString()
}
getStartLiveLoop(deviceId)
FlowBus.with<StreamMessageEvent>("start_live")
.register(this@VlcVideoPlayerActivity) { it ->
LogUtils.d(TAG, "===Received message is===" + Gson().toJson(it))
val originalLink = it.link?.trim().toString()
LogUtils.d(TAG, "===Received message url is===$originalLink")
if (originalLink.isBlank() || !originalLink.startsWith("https://")) {
Log.e(TAG, "Invalid address: $originalLink")
return@register
}
videoUrl = originalLink
playH265Video()
}
}
private fun setUpEdgeToEdge() {
val mainView = findViewById<View>(R.id.main)
ViewCompat.setOnApplyWindowInsetsListener(mainView) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
private fun initViews() {
updateButtonStates(false, false)
Log.d(TAG, "Ready")
}
private fun setupClickListeners() {
binding.btnPlay.setOnClickListener { playH265Video() }
binding.btnStop.setOnClickListener { stopH265Video() }
binding.btnPause.setOnClickListener { pauseH265Video() }
binding.btnResume.setOnClickListener { resumeH265Video() }
}
/**
* Send command to start device playback
*/
private fun getStartLiveLoop(deviceId: String) {
stopLiveLoop()
liveJob = liveScope.launch {
while (isActive) {
// Execute every 1 second
delay(1000)
val requestBody = weakHashMapOf<String, Any>(
"device_id" to deviceId,
"directive" to IotConstants.USER_START_LIVE)
val requestHeaders = weakHashMapOf(
"x-app-pkgname" to IotConstants.APP_PACKAGE_PET_GUGU,
"accept-language" to "en-us",
"x-app-id" to "10003"
)
try {
EasyHttpUtil.post(
urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_START_LIVE,
bodyParams = requestBody,
headers = requestHeaders,
clazz = Int::class.java)
.onStart {
LogUtils.d(TAG, "Start playback command sending started:")
}.catch { exception ->
errorMsg = exception.message.toString()
LogUtils.e(TAG, "Start playback command sending failed: $errorMsg")
}.first().let { result ->
when (result) {
is ApiResult.Success -> {
requestSuccess = result.data == 200
LogUtils.d(
TAG,
"Start playback command result: ${if (requestSuccess) "Success" else "Failed"}"
)
}
is ApiResult.Error -> {
throw ApiException(result.code, result.message)
}
}
}
}catch (e: Exception){
LogUtils.e(TAG,"===Request error==="+e.message)
throw e
}
}
}
}
private fun initStopVideo() {
val requestBody = weakHashMapOf<String, Any>("device_id" to deviceId, "directive" to IotConstants.USER_STOP_LIVE)
val requestHeaders = weakHashMapOf(
"x-app-pkgname" to IotConstants.APP_PACKAGE_PET_GUGU,
"accept-language" to "en-us",
"x-app-id" to "10003"
)
lifecycleScope.launch {
EasyHttpUtil.post(
urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_STOP_LIVE,
bodyParams = requestBody,
headers = requestHeaders,
clazz = Int::class.java
).onStart {
LogUtils.d(TAG, "Stop playback command sending started:")
}.catch { exception ->
val errorMsg = exception.message.toString()
LogUtils.d(TAG, "Stop playback command sending error: $errorMsg")
}.first().let { result ->
when (result) {
is ApiResult.Success -> {
requestSuccess = result.data == 200
LogUtils.d(
TAG,
"Stop playback command: ${if (requestSuccess) "Success" else "Failed"}"
)
}
is ApiResult.Error -> {
throw ApiException(result.code, result.message)
}
}
}
}
}
private fun stopLiveLoop() {
liveJob?.cancel()
}
// Play video
private fun playH265Video() {
if (!::vlcPlayer.isInitialized || !vlcPlayer.isInitialized()) {
Log.d(TAG, "Player not initialized")
return
}
if (binding.vlcVideoLayout.width == 0 || binding.vlcVideoLayout.height == 0) {
Log.e(TAG, "Playback failed: Playback control not ready")
Log.d(TAG, "Error: Playback control not ready")
return
}
try {
Log.d(TAG, "Start playing address: $videoUrl")
vlcPlayer.play(videoUrl)
isPlaying = true
isPaused = false
updateButtonStates(isPlaying, isPaused)
} catch (e: Exception) {
Log.e(TAG, "Playback failed", e)
Log.d(TAG, "Playback failed: ${e.message}")
}
}
// Stop playback
private fun stopH265Video() {
Log.d(TAG, "Stop playback")
if (!isPlaying) {
Log.d(TAG, "Not playing")
return
}
vlcPlayer.stop()
isPlaying = false
isPaused = false
updateButtonStates(isPlaying, isPaused)
}
// Pause playback
private fun pauseH265Video() {
Log.d(TAG, "Pause playback")
if (!isPlaying || isPaused) {
Log.d(TAG, "Already paused or not playing")
return
}
if (vlcPlayer.isInitialized()) {
vlcPlayer.pauseRender()
}
isPaused = true
updateButtonStates(isPlaying, isPaused)
}
// Resume playback
private fun resumeH265Video() {
Log.d(TAG, "Resume playback")
if (!isPlaying || !isPaused) {
Log.d(TAG, "Not paused or not playing")
return
}
if (vlcPlayer.isInitialized()) {
vlcPlayer.resumeRender()
}
isPaused = false
updateButtonStates(isPlaying, isPaused)
}
private fun updateButtonStates(isPlaying: Boolean, isPaused: Boolean) {
mainHandler.post {
binding.btnPlay.isEnabled = !isPlaying
binding.btnStop.isEnabled = isPlaying
binding.btnPause.isEnabled = isPlaying && !isPaused
binding.btnResume.isEnabled = isPlaying && isPaused
}
}
override fun onStart() {
super.onStart()
Log.d("VlcActivity", "onStart: Resume rendering")
if (vlcPlayer.isInitialized()) {
vlcPlayer.resumeRender()
}
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onPause")
if (vlcPlayer.isInitialized()) {
vlcPlayer.pauseRender()
}
}
/* override fun onResume() {
super.onResume()
Log.d(TAG, "onResume")
if (isPaused) {
resumeH265Video()
}
}*/
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy")
vlcPlayer.detachSurface()
vlcPlayer.release()
mainHandler.removeCallbacksAndMessages(null)
}
override fun onPrepared() {
LogUtils.d(TAG,"Preparation complete, playing...")
}
override fun onError(message: String) {
LogUtils.d(TAG,"Error: $message")
mainHandler.post {
isPlaying = false
isPaused = false
updateButtonStates(isPlaying, isPaused)
}
}
override fun onStopped() {
LogUtils.d(TAG,"Playback stopped")
mainHandler.post {
isPlaying = false
isPaused = false
updateButtonStates(isPlaying, isPaused)
}
}
override fun onProgress(currentPosition: Long, totalDuration: Long) {
}
}
10. Implementation Effect:
11. Comparison of Two Approaches:
The device pushes a complete H.264/H.265 video stream, and the App side has two processing strategies:
Approach One: Frame-by-Frame Playback
| Dimension | Processing Method |
|---|---|
| Data Parsing | Manually parse NAL unit boundaries (0x00 00 00 01 or 0x00 00 01), split I/P/B frames by nal_unit_type, maintain decoding order (DTS) and display order (PTS) |
| Buffering Strategy | Very small buffer queue (1-3 frames), "decode upon receipt, render upon decoding" |
| Clock Control | Need to maintain frame rate clock (20fps → render one frame every 50ms), precisely controlled with Choreographer or custom timer |
| Latency | Very low (50-100ms) |
| Jitter Resistance | Poor, network fluctuations directly manifest as picture stuttering/frame skipping |
| CPU Usage | High, requires frequent NALU splitting, decoding thread scheduling |
| Applicable Scenarios | Remote real-time control, low-latency intercom |
Key Difficulty: H.265 NALUs are usually larger than H.264, making frame-by-frame parsing more expensive; must also handle out-of-order, packet loss, and frame reassembly after retransmission.
Approach Two: Stream-based Playback (Current VLC Approach)
| Dimension | Processing Method |
|---|---|
| Data Parsing | VLC internal Demuxer handles automatically, automatically identifies AnnexB / AVCC format, no manual frame splitting needed |
| Buffering Strategy | Depends on network-caching + live-caching (current code set to 800ms), internally maintains ES stream buffer queue |
| Clock Control | VLC internally uses PCR/PTS for audio/video synchronization, automatically handles frame rate adaptation, frame dropping/repeating |
| Latency | Medium (300ms ~ 2s, depending on cache size) |
| Jitter Resistance | Strong, buffer queue can absorb network fluctuations |
| CPU Usage | Controllable, VLC internally optimizes the decoding pipeline |
| Applicable Scenarios | Surveillance live streaming, IoT video return (current code scenario) |
Frame-by-frame playback: Manual frame splitting → Small buffer → Precise clock → Low latency but prone to stuttering Stream-based playback: VLC automatic processing → Large buffer → Internal synchronization → Slightly higher latency but more stable
12. Root Cause Analysis of Slow Start, Stuttering, and Black Screen
Slow Start (3-5 seconds or longer)
| Root Cause | Description |
|---|---|
| Excessive Cache | Current --network-caching=800 / --live-caching=800, VLC accumulates 800ms of data before starting playback |
| Waiting for Keyframe | H.265 encoding is in GOP units, the player must wait for an IDR frame to start decoding and rendering |
| Protocol Handshake | RTSP requires DESCRIBE → SETUP → PLAY, HTTP requires TLS handshake + request/response |
| Slow Software Decoding Initialization | --avcodec-hw=none forces software decoding, H.265 decoder initialization time is significantly longer than H.264 |
| Surface Delayed Binding | In setSurface(), when width/height is 0, it uses post { } delayed binding, increasing start time |
Stuttering / Black Screen
| Root Cause | Description |
|---|---|
| Cache Too Small | If cache is set too small, buffer depletes during network jitter, directly causing stream interruption and stuttering |
| Insufficient Software Decoding Performance | H.265 software decoding is computationally heavy, low-end devices cannot decode fast enough for the bitrate, causing frame drops |
| Delayed Frame Accumulation | --drop-late-frames is not currently configured, expired frames accumulate causing increasing delay |
| Hardware Decoding Compatibility | Some devices have black/flashing screen issues with H.265 hardware decoding (--avcodec-hw=none in the code is to avoid this) |
| No Event Listener | setEventListener is commented out in the code, unable to perceive error states like EncounteredError |
13. Summary:
13.1 Expected Optimization Effects:
| Metric | Before Optimization | After Optimization (Strong Network) | After Optimization (Weak Network) |
|---|---|---|---|
| Start Time | 3-5s | 0.5-1s | 1-2s |
| End-to-End Latency | 1.5-3s | 200-400ms | 800ms-1.5s |
| Stutter Rate | Medium | Low (requires stable network) | Low (buffer protection) |
| Black Screen Rate | High (no error awareness) | Low (event listener + auto degradation) | Low (forced software decoding) |
13.2 Conclusion:
I am currently using H.265 format decoding. Initially using H.264, I found the effect was not very good, and after confirming with the device side that the protocol used is also H.265, I switched to H.265. The frame rate set is adjusted according to the protocol transmitted by the device. The network buffer time and live stream specific buffer time can both be dynamically adjusted. Of course, there are other players and solutions, and parameters are adjusted as needed. The above implementation is a summary of my own project. Everyone is welcome to actively discuss. In the AI era, cherish technology and take it step by step.