Cara Menampilkan CCTV di Android
Cara menampilkan CCTV menggunakan ExoPlayer di Android
Pengenalan
Seperti yang kita ketahui bahwa di Android untuk bisa melakukan pemutaran video kita bisa menggunakan VideoView. Namun, seiring berjalannya waktu dan perkembangan Android-nya sendiri maka penggunaan VideoView akhirnya memiliki alternatifnya tersendiri yang lebih powerful, yaitu ExoPlayer. ExoPlayer merupakan pustaka buatannya Google yang memiliki fungsi lebih dari sekedar video player. Berikut projek Github-nya.
Seperti biasanya dikarenakan ExoPlayer merupakan projek open source dari Google maka, sudah dipastikan pengembangannya lebih cepat. Jika kita sering mengikuti perkembangan-perkembangan yang diumumkan di acara Google I/O seharusnya kita sudah tahu bahwa ExoPlayer diperkenalkan pada acara Google I/O 17.
ExoPlayer dibuat untuk mempermudah penggunaan Media Player di Android seperti melakukan pemutaran video, audio dan dukungan pemakaiannya yang dilakukan untuk pemutaran media player secara file lokal dan internet.
Dan berikut ialah beberapa fitur yang didukung oleh si ExoPlayer.
CCTV
Di Android untuk bisa melakukan live streaming CCTV sebenarnya ada 2 cara yang saya ketahui yaitu yang pertama kita bisa menggunakan SDK-nya bawaan si vendor CCTV (jika tersedia) dan yang kedua kita bisa menggunakan ExoPlayer. Namun, yang saya ketahui untuk ExoPlayer kita hanya bisa menampilkan CCTV yang publik saja (tanpa autentifikasi) dan jika terdapat autentifikasi maka kita harus menggunakan SDK bawaannya si vendor contohnya seperti yang sudah pernah saya coba yaitu HikVision dan pustaka yang saya pakai adalah sebagai berikut.
Lalu untuk CCTV yang publik kita bisa mengaksesnya dengan cara langsung memanggil url dari si CCTV tersebut. Untuk contoh CCTV publik yang akan saya jadikan contoh adalah CCTV dari kota Medan http://smartprovince.sumutprov.go.id dimana dari link tersebut kita bisa mengakses menu CCTV. Lalu di bagian web-nya coba kita inspect element pada halaman webnya maka nanti disitu kita bisa lihat url dari CCTV-nya seperti berikut.
Tapi tenang saja untuk semua url CCTV-nya sudah saya masukkan ke dalam server fake json buatan saya jadi kita cukup memanggil endpoint fake json-nya saja.
http://api.bengkelrobot.net:8001/cctv
Mulai Pembuatan Projek
Sekarang mari kita mulai langkah-langkah pembuatan projeknya.
Setup build.gradle file
Pertama-tama kita perlu menambahkan beberapa dependency yang kita butuhkan seperti berikut.
implementation 'com.google.android.exoplayer:exoplayer:2.10.2'
implementation 'com.google.android.material:material:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-M1'
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
Tambahkan izin permisi INTERNET
Dikarenakan aplikasi kita membutuhkan penggunaan internet maka, di file AndroidManifest.xml kita tambahkan izin permisi internetnya seperti berikut.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.ysn.cctvexo">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Buat UI activity_main.xml
Selanjutnya, di layout activity_main.xml kita perlu membuat sebuah tampilan yang menampilkan list dari CCTV-nya menggunakan RecyclerView.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/recycler_view_cctv_activity_main"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progress_bar_loading_activity_main"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Buat class model API
Selanjutnya kita perlu membuat class model untuk melakukan parsing dari respon JSON endpoint list cctv-nya. Berikut respon dari endpoint-nya.
{
"data": [
{
"name": "Air Mancur",
"url": "http://110.232.82.176/live/AIR_MANCUR.m3u8"
},
{
"name": "Putri Hijau",
"url": "http://110.232.82.176/live/CCTV_PUTRI_HIJAU-TVRI.m3u8"
},
{
"name": "Tirtanadi",
"url": "http://110.232.82.176/live/TIRTONADI.m3u8"
},
{
"name": "Dr Mansyur",
"url": "http://110.232.82.176/live/JM_DR_MANSYUR.m3u8"
},
{
"name": "Jalan Gagak Hitam Amal 1",
"url": "http://110.232.82.176/live/AMAL_GAGAKHITAM1.m3u8"
},
{
"name": "Majestik",
"url": "http://110.232.82.176/live/MAJESTIK.m3u8"
},
{
"name": "BII",
"url": "http://110.232.82.176/live/BII.m3u8"
},
{
"name": "Jalan Ring Road Asoka 1",
"url": "http://110.232.82.176/live/ASOKA_GAGAKHITAM1.m3u8"
},
{
"name": "Jalan Ring Road Asoka 2",
"url": "http://110.232.82.176/live/ASOKA_GAGAKHITAM2.m3u8"
},
{
"name": "Jalan Ring Road Asoka 3",
"url": "http://110.232.82.176/live/ASOKA_GAGAKHITAM3.m3u8"
},
{
"name": "Jalan Simpang Ayahanda",
"url": "http://110.232.82.176/live/AYAHANDA.m3u8"
},
{
"name": "TVRI",
"url": "http://110.232.82.176/live/TVRI.m3u8"
},
{
"name": "Kartini",
"url": "http://110.232.82.176/live/KARTINI.m3u8"
},
{
"name": "Jalan Kesawan",
"url": "http://110.232.82.176/live/CCTV_KESAWAN.m3u8"
},
{
"name": "Walikota Medan",
"url": "http://110.232.82.176/live/CCTV_DPN_WALIKOTA.m3u8"
},
{
"name": "Istana Plaza",
"url": "http://110.232.82.176/live/ISTANA_PLAZA.m3u8"
},
{
"name": "Amplas",
"url": "http://110.232.82.176/live/AMPLAS.m3u8"
},
{
"name": "Simpang Masjid Raya",
"url": "http://110.232.82.176/live/MASJID_RAYA.m3u8"
},
{
"name": "Istana Maimun",
"url": "http://110.232.82.176/live/ISTANA_MAIMUN.m3u8"
},
{
"name": "Jalan Perintis Kemerdekaan",
"url": "http://110.232.82.176/live/SC_SUTOMO-PERINTIS.m3u8"
},
{
"name": "Jalan Setia Budi Industri",
"url": "http://110.232.82.176/live/SETIABUDI_INDUSTRI1.m3u8"
},
{
"name": "Jalan Simpang Pos 1",
"url": "http://110.232.82.176/live/SIMPANG_POS1.m3u8"
},
{
"name": "Jalan Simpang Pos 2",
"url": "http://110.232.82.176/live/SIMPANG_POS2.m3u8"
},
{
"name": "Jalan Sei Sikambing",
"url": "http://110.232.82.176/live/SEI_SIKAMBING.m3u8"
},
{
"name": "Tol Amplas 1",
"url": "http://110.232.82.176/live/TOL_AMPLAS1.m3u8"
},
{
"name": "Stasiun KA",
"url": "http://110.232.82.176/live/STASIUN_KA.m3u8"
},
{
"name": "Jalan Yos Sudarso Pembangungan",
"url": "http://110.232.82.176/live/YOS_SUDARSO-PEMBANGUNAN.m3u8"
},
{
"name": "Simpang Waspada",
"url": "http://110.232.82.176/live/WASPADA.m3u8"
},
{
"name": "Medan Plaza",
"url": "http://110.232.82.176/live/MEDAN_PLAZA.m3u8"
},
{
"name": "Gatot Subroto Medan Plaza",
"url": "http://110.232.82.176/live/CCTV_GATSU-MEDAN_PLAZA.m3u8"
},
{
"name": "Tugu Adipura",
"url": "http://110.232.82.176/live/CCTV_TUGU_ADIPURA.m3u8"
},
{
"name": "SM Raja-Ramayana",
"url": "http://110.232.82.176/live/CCTV_SM_RAJA_RAMAYANA.m3u8"
},
{
"name": "Depan Millenium",
"url": "http://110.232.82.176/live/CCTV_DPN_MILLENIUM.m3u8"
},
{
"name": "Merdeka",
"url": "http://110.232.82.176/live/MERDEKA1.m3u8"
},
{
"name": "HDTI",
"url": "http://110.232.82.176/live/HDTI.m3u8"
},
{
"name": "Depan Kantor Gubernur",
"url": "http://110.232.82.176/live/CCTV_DPN_KANTOR_GUBERNUR.m3u8"
},
{
"name": "HM Said Perintis",
"url": "http://110.232.82.176/live/CCTV_HM_SAID_PERINTIS.m3u8"
}
]
}
Maka kita buat class model-nya seperti berikut.
package com.ysn.cctvexo.model
data class CctvData(
val data: List<Data>
)
data class Data(
val name: String,
val url: String
)
Buat interface API
Kemudian, kita perlu membuat interface API dari endpoint-nya seperti berikut.
package com.ysn.cctvexo.api
import com.ysn.cctvexo.model.CctvData
import retrofit2.Response
import retrofit2.http.GET
interface CctvApi {
@GET("cctv")
suspend fun getListCctv(): Response<CctvData>
}
Buat layout item_list_cctv.xml
Karena kita akan menampilkannya didalam RecyclerView maka kita perlu membuat adapternya dan adapternya memerlukan item layout. Maka berikut item layout-nya.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/linear_layout_container_item_list_cctv"
android:orientation="vertical"
android:padding="16dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_view_name_item_list_cctv"
android:textSize="16sp"
tools:text="CCTV Medan"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/text_view_url_item_list_cctv"
android:textColor="@android:color/darker_gray"
tools:text="http://www.sample.com/live"
/>
</LinearLayout>
Buat Adapter si RecyclerView
Selanjutnya kita buat adapter untuk si RecyclerView-nya.
package com.ysn.cctvexo.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.ysn.cctvexo.R
import com.ysn.cctvexo.model.Data
import kotlinx.android.synthetic.main.item_list_cctv.view.*
class AdapterListCctv constructor(
private val listCctv: List<Data>,
private val listener: ListenerAdapterListCctv
) : RecyclerView.Adapter<AdapterListCctv.ViewHolderItemCctv>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderItemCctv {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_list_cctv, parent, false)
return ViewHolderItemCctv(itemView)
}
override fun onBindViewHolder(holder: ViewHolderItemCctv, position: Int) {
val data = listCctv[position]
holder.bind(data)
}
override fun getItemCount(): Int {
return listCctv.size
}
inner class ViewHolderItemCctv constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.linear_layout_container_item_list_cctv.setOnClickListener {
listener.onClickItem(listCctv[adapterPosition])
}
}
fun bind(data: Data) {
itemView.text_view_name_item_list_cctv.text = data.name
itemView.text_view_url_item_list_cctv.text = data.url
}
}
interface ListenerAdapterListCctv {
fun onClickItem(data: Data)
}
}
Buat logic menampilkan List CCTV
Sekarang kita buka file MainActivity.kt dan isi dengan kode berikut.
package com.ysn.cctvexo
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.ysn.cctvexo.adapter.AdapterListCctv
import com.ysn.cctvexo.api.CctvApi
import com.ysn.cctvexo.model.Data
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MainActivity : AppCompatActivity() {
private lateinit var retrofit: Retrofit
private lateinit var cctvApi: CctvApi
private lateinit var adapterListCctv: AdapterListCctv
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRetrofit()
doLoadListCctv()
}
private fun doLoadListCctv() {
progress_bar_loading_activity_main.visibility = View.VISIBLE
recycler_view_cctv_activity_main.visibility = View.GONE
CoroutineScope(Dispatchers.IO).launch {
val response = cctvApi.getListCctv()
withContext(Dispatchers.Main) {
try {
if (response.isSuccessful) {
val cctvData = response.body()!!
adapterListCctv =
AdapterListCctv(cctvData.data, object : AdapterListCctv.ListenerAdapterListCctv {
override fun onClickItem(data: Data) {
// TODO: do something in here
}
})
recycler_view_cctv_activity_main.layoutManager = LinearLayoutManager(this@MainActivity)
recycler_view_cctv_activity_main.addItemDecoration(
DividerItemDecoration(
this@MainActivity,
DividerItemDecoration.VERTICAL
)
)
recycler_view_cctv_activity_main.adapter = adapterListCctv
progress_bar_loading_activity_main.visibility = View.GONE
recycler_view_cctv_activity_main.visibility = View.VISIBLE
} else {
progress_bar_loading_activity_main.visibility = View.GONE
recycler_view_cctv_activity_main.visibility = View.GONE
Toast.makeText(this@MainActivity, "Error: ${response.code()}", Toast.LENGTH_SHORT)
.show()
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "Exception: ${e.message}", Toast.LENGTH_SHORT)
.show()
}
}
}
}
private fun initRetrofit() {
retrofit = Retrofit.Builder()
.baseUrl("http://api.bengkelrobot.net:8001/")
.addConverterFactory(GsonConverterFactory.create())
.build()
cctvApi = retrofit.create(CctvApi::class.java)
}
}
Pada kode diatas flow-nya adalah seperti berikut.
- Pertama kita melakukan inisialisasi untuk objek Retrofit-nya.
- Lalu, di fungsi
doLoadListCctv()
kita ada melakukan pemanggilan ke endpoint fake json untuk mendapatkan data list CCTV yang akan kita tampilkan kedalam RecyclerView.
Jika berhasil maka tampilan programnya akan menjadi seperti berikut.
Buat UI Menampilkan CCTV
Pada langkah sebelumnya kita telah berhasil membuat fitur untuk menampilkan list CCTV-nya. Maka, untuk langkah selanjutnya adalah kita perlu memberikan event onClickListener untuk setiap item di RecyclerView-nya. Caranya, silakan kita buat activity baru dengan nama CctvViewActivity.kt (beserta dengan layout-nya). Kemudian didalam layout-nya kita isikan dengan kode berikut.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CctvViewActivity"
>
<com.google.android.exoplayer2.ui.PlayerView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/player_view_activity_cctv_view"
android:background="#212121"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Buat logic menampilkan kamera CCTV
Sekarang kita buka file CctvViewActivity.kt dan isi dengan kode berikut.
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import kotlinx.android.synthetic.main.activity_cctv_view.*
class CctvViewActivity : AppCompatActivity() {
private var player: SimpleExoPlayer? = null
private var playbackPosition: Long? = null
private var currentWindow: Int? = null
private var playWhenReady: Boolean? = null
private var url: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cctv_view)
setTitleToolbar()
doLoadDataExtras()
setLandscapeOrientation()
}
override fun onResume() {
initializePlayer()
super.onResume()
}
override fun onPause() {
releasePlayer()
super.onPause()
}
private fun initializePlayer() {
if (player == null) {
player = ExoPlayerFactory.newSimpleInstance(
this,
DefaultRenderersFactory(this),
DefaultTrackSelector(),
DefaultLoadControl()
)
player!!.playWhenReady = true
player_view_activity_cctv_view.player = player
}
val mediaSource = buildMediaSource(Uri.parse(url))
player!!.prepare(mediaSource, true, false)
}
private fun releasePlayer() {
if (player != null) {
playbackPosition = player?.currentPosition
currentWindow = player?.currentWindowIndex
playWhenReady = player?.playWhenReady
player?.release()
player == null
}
}
private fun buildMediaSource(uri: Uri): MediaSource {
val userAgent = "exoplayer-codelab"
if (uri.lastPathSegment.contains("mp3") || uri.lastPathSegment.contains("mp4")) {
return ExtractorMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else if (uri.lastPathSegment.contains("m3u8")) {
return HlsMediaSource.Factory(DefaultHttpDataSourceFactory(userAgent))
.createMediaSource(uri)
} else {
val dashChunkSourceFactor = DefaultDashChunkSource.Factory(DefaultHttpDataSourceFactory("ua"))
val manifestDataSourceFactory = DefaultHttpDataSourceFactory(userAgent)
return DashMediaSource.Factory(dashChunkSourceFactor, manifestDataSourceFactory).createMediaSource(uri)
}
}
private fun setLandscapeOrientation() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
private fun setTitleToolbar() {
supportActionBar?.title = "Live Streaming CCTV"
}
private fun doLoadDataExtras() {
val bundle = intent?.extras
if (bundle != null) {
url = bundle.getString("url", "")
}
}
}
Pada kode diatas jika kita perhatikan terdapat kode untuk mengambil url yang dikirim dari activity sebelumnya maka, kita buka lagi file MainActivity.kt dan pada method onClickItem()
didalamnya kita tambahkan kode berikut.
val intentCctvViewActivity = Intent(this@MainActivity, CctvViewActivity::class.java)
intentCctvViewActivity.putExtra("url", data.url)
startActivity(intentCctvViewActivity)
Sekarang coba test jalankan lagi lalu, pada aplikasi coba kita tap pada salah satu cctv yang ingin kita lihat.
Kesimpulan
Jadi, setelah kita berhasil menampilkan CCTV secara live streaming menggunakan ExoPlayer maka, dapat diambil kesimpulan bahwa penggunaan ExoPlayer menurut saya lebih mudah dan powerful.