Skip to main content

Native Android

The easiest way to view Vindral streams in native Android applications is to embed the hosted player with a WebView.

The example code below is simplified to focus on the relevant parts.

class MainActivity : AppCompatActivity() {
lateinit var webView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)

webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false

webView.webChromeClient = FullscreenChromeClient()

webView.loadUrl("https://player.vindral.com/?channelId=vindral_demo1_ci_099ee1fa-80f3-455e-aa23-3d184e93e04f")
}

private inner class FullscreenChromeClient : WebChromeClient() {
private var customView: View? = null
private var customViewCallback: CustomViewCallback? = null
private var mOriginalOrientation = 0

override fun onHideCustomView() {
(window.decorView as FrameLayout).removeView(customView)
customView = null
customViewCallback!!.onCustomViewHidden()
customViewCallback = null
requestedOrientation = mOriginalOrientation
showSystemUI()
}

override fun onShowCustomView(view: View, callback: CustomViewCallback) {
if (customView != null) {
onHideCustomView()
return
}
customView = view
mOriginalOrientation = requestedOrientation
customViewCallback = callback
(window.decorView as FrameLayout).addView(customView)
hideSystemUI()
setOrientationToLandscape()
}

private fun hideSystemUI() {
val decorView = window.decorView
if (Build.VERSION.SDK_INT >= 30) {
decorView.windowInsetsController!!.hide(WindowInsets.Type.statusBars())
} else {
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
}

private fun showSystemUI() {
val decorView = window.decorView
if (Build.VERSION.SDK_INT >= 30) {
decorView.windowInsetsController!!.show(WindowInsets.Type.statusBars())
} else {
decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
}

private fun setOrientationToLandscape() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}

}

Communication between Native and WebView

The following is a demonstration of how to establish communication between the Vindral web SDK and native code. The example illustrates how to play and pause, as well as how to monitor certain events such as playback state, volume state, and errors.

For a comprehensive guide on the functions and events of the Web SDK, refer to the Web SDK documentation.

You can find a complete example project on GitHub.

enum class VindralPlaybackState(val state: String) {
PLAYING("playing"),
BUFFERING("buffering"),
PAUSED("paused")
}
data class VindralVolumeState(val isMuted: Boolean, val volume: Double)
data class VindralError(val code: String, val message: String, val isFatal: Boolean)

class MainActivity : ComponentActivity() {
private val gson = Gson()
private lateinit var webView: WebView
private var url =
"https://player.vindral.com/?core.url=https://lb.cdn.vindral.com&core.channelId=vindral_demo1_ci_099ee1fa-80f3-455e-aa23-3d184e93e04f"
companion object {
private const val JS_INTERFACE = "Android"
private const val JS_CODE =
"""
async function run() {
let loops = 0
while (!window.vindral) {
if (loops > 20) {
throw new Error("Could not find window.vindral")
}
await new Promise(r => setTimeout(r, 100))
loops++
}

window.vindral.on("playback state", (state) => window.$JS_INTERFACE.onPlaybackState(state))
window.vindral.on("volume state", (state) => window.$JS_INTERFACE.onVolumeState(JSON.stringify(state)))
window.vindral.on("error", (error) => window.$JS_INTERFACE.onError(JSON.stringify(error)))
}
run()
"""
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WebViewWithControls(url)
}
}
}
}

private fun play() {
webView.evaluateJavascript("window.vindral.play()", null)
}

private fun pause() {
webView.evaluateJavascript("window.vindral.pause()", null)
}

@android.webkit.JavascriptInterface
fun onPlaybackState(state: String) {
val playbackState = VindralPlaybackState.valueOf(state.uppercase())
Toast.makeText(
this,
"Playback state: ${playbackState.state}",
Toast.LENGTH_SHORT
).show()
}

@android.webkit.JavascriptInterface
fun onVolumeState(json: String) {
val volumeState = gson.fromJson(json, VindralVolumeState::class.java)
Toast.makeText(
this,
"Volume state: isMuted=${volumeState.isMuted}, volume=${volumeState.volume}",
Toast.LENGTH_SHORT
).show()
}

@android.webkit.JavascriptInterface
fun onError(json: String) {
val error = gson.fromJson(json, VindralError::class.java)
Toast.makeText(this, "Error: $error", Toast.LENGTH_SHORT).show()
}

@Composable
fun WebViewComposable(url: String) {
this.webView = remember {
WebView(this).apply {
settings.javaScriptEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
addJavascriptInterface(this@MainActivity, JS_INTERFACE)

webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
webView.evaluateJavascript(
JS_CODE,
null
)
}
}
loadUrl(url)
}
}
webView.layoutParams = android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT
)
AndroidView({ webView })
}

@Composable
fun WebViewWithControls(url: String) {
Box(modifier = Modifier.fillMaxSize()) {
WebViewComposable(url)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp)
.background(Color.Black.copy(alpha = 0.7f))
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.Center
) {
Button(onClick = {
play()
}) {
Text("Play")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = {
pause()
}) {
Text("Pause")
}
}
}
}
}