How to integrate VLC OR vlcLib in Android Kotlin
There is pretty much no full guide or documentation on this topic at the time of writing this article and I faced many challenges myself to integrate the VLC library in my Android project so that I could play a live stream video via it. There is also no proper documentation on the VLC site related to how to properly integrate it, just one sample.
so here is the full guide after one week of deep R&D on this topic
Add the below dependency in your app gradle file
implementation("org.videolan.android:libvlc-all:3.1.12")
In my requirement, I had to play a live stream video, but the same code can be used for other sources as well whether it be for video locally stored in your project or just video stored in my server.
Note for the videos which are stored in your project, you have to provide a path for it instead of a url
Below is the main initialization method
import org.videolan.libvlc.LibVLC
private var libVLC: LibVLC? = null
private var vlcMediaPlayer: org.videolan.libvlc.MediaPlayer? = null
private fun initializeVlcVideoPlayer(videoUrl: String?) {
videoUrl?.let { url ->
layoutVideoWidgetBinding?.let { binding ->
val args = arrayListOf(
"--file-caching=150",
"--network-caching=150",
"--clock-jitter=0",
"--live-caching=150",
"--drop-late-frames",
"--skip-frames",
"--vout=android-display",
"--sout-transcode-vb=20",
"--no-audio",
"--sout=#transcode{vcodec=h264,vb=20,acodec=mpga,ab=128,channels=2,samplerate=44100}:duplicate{dst=display}",
"--sout-x264-nf"
)
libVLC = LibVLC(binding.root.context, args)
vlcMediaPlayer = org.videolan.libvlc.MediaPlayer(libVLC)
vlcMediaPlayer?.attachViews(binding.vlcVideoLayout, null, false, false)
vlcMediaPlayer?.setEventListener { event ->
handleVlcEvents(event, binding)
}
setVlcMedia(url)
binding.vlcVideoLayout.visible()
}
}
}
Vlc provides a rich amount of arguments through which we can customize our videos based on the requirements though with a pretty shit level of documentation.
The arguments used in the above example do the following.
--file-caching=150
: Sets the amount of time, in milliseconds, that the media player should cache media files read from the disk before playing them. A higher value can lead to smoother playback on slower storage devices.
--network-caching=150
: Specifies the amount of time, in milliseconds, to cache media data from network sources. Increasing this value can improve playback smoothness for network streams at the cost of increased latency.
--clock-jitter=0
: Sets the clock jitter compensation in milliseconds. A value of 0 can be used to disable clock jitter compensation. Clock jitter compensation helps to synchronize audio and video playback.
--live-caching=150
: Determines the caching time, in milliseconds, for live capture streams, such as live internet broadcasts or live input from a capture device. This helps in smoothing out live playback.
--drop-late-frames
: Instructs the player to drop frames that arrive late. This can help to keep audio and video in sync, especially in situations where the system cannot keep up with the playback requirements.
--skip-frames
: Allows the media player to skip video frames to maintain the speed of playback. This is useful when the decoding process is slower than the video playback speed, potentially keeping audio and video more synchronized.
--vout=android-display
: Specifies the video output module to use. In this case, it’s set for Android display, indicating the output is optimized for display on Android devices.
--sout-transcode-vb=20
: Sets the video bitrate for transcoding to 20 kb/s. This option is part of a transcoding command that adjusts the quality of the video stream.
--no-audio
: Disables audio playback. This is useful when you only want to deal with the video stream or when audio is unnecessary or needs to be processed separately.
--sout=#transcode{vcodec=h264,vb=20,acodec=mpga,ab=128,channels=2,samplerate=44100}:duplicate{dst=display}
: This complex option specifies a stream output chain that transcodes the video to H.264 codec with a bitrate of 20 kb/s, and audio to MPEG Audio (MPGA) with a bitrate of 128 kb/s, 2 channels, and a sample rate of 44100 Hz, then duplicates the output to display. It is used for converting and streaming media in a different format.
--sout-x264-nf
: Disables x264’s noise filtering. This option is part of the video transcoding settings, specifically for the H.264 codec, which can impact the visual quality of the video. Disabling noise filtering might result in a less smooth video but can improve encoding speed or maintain more detail in the video.
You can find all the available argument options here
Method to set media for vlcMediaPlayer
private fun setVlcMedia(videoUrl: String) {
val media = org.videolan.libvlc.Media(libVLC, Uri.parse(videoUrl))
vlcMediaPlayer?.media = media
vlcMediaPlayer?.play()
media.release() // Release the media object once it is attached to the player
}
To run the file that is locally stored in your project let's say in an assets folder use the below snippet, the rest will remain the same.
Media(mLibVLC, getAssets().openFd(ASSET_FILENAME))
And the XML tag
<org.videolan.libvlc.util.VLCVideoLayout
android:id="@+id/vlc_video_layout"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
And the events listeners that VLC provides
private fun handleVlcEvents(
event: org.videolan.libvlc.MediaPlayer.Event,
binding: LayoutVideoWidgetBinding
) {
when (event.type) {
org.videolan.libvlc.MediaPlayer.Event.Playing -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Playing")
}
org.videolan.libvlc.MediaPlayer.Event.Paused -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Paused")
}
org.videolan.libvlc.MediaPlayer.Event.Stopped -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Stopped")
}
org.videolan.libvlc.MediaPlayer.Event.Buffering -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Buffering")
}
org.videolan.libvlc.MediaPlayer.Event.EncounteredError -> {
binding.loaderLa.visible()
CoroutineScope(Dispatchers.Main).launch {
delay(500)
refreshVideoUrl(videoUrl)
}
Log.d(TAG, "VLC Event Error")
}
org.videolan.libvlc.MediaPlayer.Event.EndReached -> {
binding.loaderLa.visible()
CoroutineScope(Dispatchers.Main).launch {
delay(500)
refreshVideoUrl(videoUrl)
}
Log.d(TAG, "VLC Event End Reached")
}
org.videolan.libvlc.MediaPlayer.Event.Opening -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Opening")
}
org.videolan.libvlc.MediaPlayer.Event.TimeChanged -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Time Changed")
}
org.videolan.libvlc.MediaPlayer.Event.PositionChanged -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Position Changed")
}
org.videolan.libvlc.MediaPlayer.Event.SeekableChanged -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Seekable Changed")
}
org.videolan.libvlc.MediaPlayer.Event.PausableChanged -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Pausable Changed")
}
org.videolan.libvlc.MediaPlayer.Event.LengthChanged -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Length Changed")
}
org.videolan.libvlc.MediaPlayer.Event.Vout -> {
binding.loaderLa.gone()
Log.d(TAG, "VLC Event Video Output")
}
org.videolan.libvlc.MediaPlayer.Event.ESAdded -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Elementary Stream Added")
}
org.videolan.libvlc.MediaPlayer.Event.ESDeleted -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Elementary Stream Deleted")
}
org.videolan.libvlc.MediaPlayer.Event.ESSelected -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Elementary Stream Selected")
}
else -> {
binding.loaderLa.visible()
Log.d(TAG, "VLC Event Other: ${event.type}")
}
}
}
RefreshVideoUrl method calls the setMedia() method again, you may not this part of the code, it was a requirement that I had to implement because of the live stream source I was using.
And if you are using pro-guide then you might face an error/crash for the release app, for that place below snippet in your app/proguard-rules.pro
-keep class org.videolan.libvlc.** { *; }
-keep interface org.videolan.libvlc.** { *; }
-dontwarn org.videolan.libvlc.**
Use the below method to clear up the resource whether in onDestory() or wherever you like
private fun releaseVlcPlayer() {
vlcMediaPlayer?.release()
vlcMediaPlayer?.media?.release()
libVLC?.release()
vlcMediaPlayer = null
libVLC = null
}
fun clearVlcPlayerMedia() {
vlcMediaPlayer?.stop()
vlcMediaPlayer?.media?.release()
}
That’s pretty much it :) bye 👋