Integrated JWPlayer

implemented playerView on both fragments theme 1 and theme 2

Full screen activity LiveStreamPlayerActivity
This commit is contained in:
2024-05-30 21:19:25 +05:30
parent 2182c0393d
commit 5bc2b44bc1
13 changed files with 399 additions and 73 deletions

View File

@@ -47,6 +47,9 @@ android {
}
}
ext.jwPlayerVersion = '4.6.0'
ext.exoplayerVersion = '2.19.1'
dependencies {
implementation "com.airbnb.android:lottie:6.4.0"
implementation "androidx.core:core-splashscreen:1.0.1"
@@ -74,6 +77,10 @@ dependencies {
// circle image view
implementation 'de.hdodenhof:circleimageview:3.1.0'
// jwplayer
implementation "com.jwplayer:jwplayer-core:$jwPlayerVersion"
implementation "com.jwplayer:jwplayer-common:$jwPlayerVersion"
implementation libs.androidx.core.ktx
implementation libs.androidx.appcompat
implementation libs.material

View File

@@ -15,11 +15,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.Woka"
tools:targetApi="31">
<activity
android:name=".players.LiveStreamPlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:exported="false"
android:launchMode="singleTask"
android:screenOrientation="sensorLandscape"
android:supportsPictureInPicture="true"
android:theme="@style/FullScreenTheme" />
<activity
android:name=".home.ExploreWokaActivity"
android:exported="false"
android:theme="@style/TransparentActivity"
android:screenOrientation="portrait"/>
android:screenOrientation="portrait"
android:theme="@style/TransparentActivity" />
<activity
android:name=".home.HomeActivity"
android:exported="false"

View File

@@ -26,6 +26,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.jwplayer.pub.api.license.LicenseUtil
import com.woka.BuildConfig
import com.woka.R
import com.woka.WokaApp.Companion.userPrefs
@@ -128,6 +129,8 @@ class HomeActivity : WokaBaseActivity(),
if (userPrefs?.userLiveData?.isInitialized == false){
userPrefs?.loadUserData()
}
LicenseUtil().setLicenseKey(this, "LkYoNusv+gSIVJIrXa5Bf59iBNlUMxeg82PM/l8JWk+cD4BE")
}
override fun onResume() {

View File

@@ -1,6 +1,5 @@
package com.woka.home.fragments
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.BroadcastReceiver
@@ -8,16 +7,17 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_TIME_TICK
import android.content.IntentFilter
import android.net.Uri
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.woka.R
import com.woka.WokaApp.Companion.userPrefs
import com.woka.databinding.FragmentHome1Binding
@@ -25,11 +25,13 @@ import com.woka.home.HomeViewModel
import com.woka.home.TimePeriod
import com.woka.mvvm.userDataModels.UserDataResponse
import com.woka.networking.ApiResult
import com.woka.players.LiveStreamPlayerActivity
import com.woka.utils.UserType
import com.woka.utils.hide
import com.woka.utils.scaleAnimate
import com.woka.utils.show
class Home1Fragment : Fragment() {
private lateinit var binding: FragmentHome1Binding
@@ -44,6 +46,8 @@ class Home1Fragment : Fragment() {
private var star1Animator: ValueAnimator? = null
private var star2Animator: ValueAnimator? = null
private lateinit var player: ExoPlayer
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -51,8 +55,11 @@ class Home1Fragment : Fragment() {
binding = FragmentHome1Binding.inflate(inflater, container, false)
activity?.let {
viewModel = ViewModelProvider(it)[HomeViewModel::class.java]
player = ExoPlayer.Builder(it).build()
}
initPlayerView()
handleScaleAnimations()
updateBackground()
@@ -77,6 +84,9 @@ class Home1Fragment : Fragment() {
override fun onResume() {
super.onResume()
handleAnimations()
if (!player.isPlaying){
player.play()
}
}
override fun onPause() {
@@ -86,6 +96,26 @@ class Home1Fragment : Fragment() {
if (cloud2Animator?.isRunning == true) cloud2Animator?.pause()
if (star1Animator?.isRunning == true) star1Animator?.pause()
if (star2Animator?.isRunning == true) star2Animator?.pause()
if (player.isPlaying) player.pause()
}
override fun onDestroy() {
super.onDestroy()
player.release()
}
private fun initPlayerView() {
binding.playerView.player = player
binding.playerView.useController = false
player.volume = 0f
val videoUri = Uri.parse("https://d3volyx7jx7oal.cloudfront.net/master.m3u8")
val mediaItem = MediaItem.fromUri(videoUri)
player.setMediaItem(mediaItem)
player.playWhenReady = true
player.prepare()
}
private fun setObservers() {
@@ -114,7 +144,11 @@ class Home1Fragment : Fragment() {
private fun clickEvents() {
binding.apply {
blackImage.setOnClickListener {
activity?.let {
startActivity(Intent(it, LiveStreamPlayerActivity::class.java))
}
}
}
}
@@ -135,7 +169,7 @@ class Home1Fragment : Fragment() {
}
}
private fun handleScaleAnimations(){
private fun handleScaleAnimations() {
binding.apply {
webSeriesLl.post {
webSeriesLl.scaleAnimate()
@@ -164,67 +198,69 @@ class Home1Fragment : Fragment() {
}
private fun handleAnimations() {
if (tvAnimator == null) {
binding.tvView.post {
val endMargin: Float =
25f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
tvAnimator = ObjectAnimator.ofFloat(
binding.tvView,
"translationX",
resources.displayMetrics.widthPixels - binding.tvView.width - (2 * endMargin)
).apply {
duration = 12000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
synchronized(this){
if (tvAnimator == null) {
binding.tvView.post {
val endMargin: Float =
25f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
tvAnimator = ObjectAnimator.ofFloat(
binding.tvView,
"translationX",
resources.displayMetrics.widthPixels - binding.tvView.width - (2 * endMargin)
).apply {
duration = 12000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
}
}
} else if (tvAnimator?.isPaused == true) {
tvAnimator?.resume()
}
} else if (tvAnimator?.isPaused == true){
tvAnimator?.resume()
}
if (cloud1Animator == null) {
binding.cloud1.post {
val cloud1Width: Float =
900f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
cloud1Animator = ObjectAnimator.ofFloat(
binding.cloud1,
"translationX",
-cloud1Width + resources.displayMetrics.widthPixels
).apply {
duration = 120_000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
if (cloud1Animator == null) {
binding.cloud1.post {
val cloud1Width: Float =
900f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
cloud1Animator = ObjectAnimator.ofFloat(
binding.cloud1,
"translationX",
-cloud1Width + resources.displayMetrics.widthPixels
).apply {
duration = 120_000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
}
}
} else if (cloud1Animator?.isPaused == true) {
cloud1Animator?.resume()
}
} else if (cloud1Animator?.isPaused == true){
cloud1Animator?.resume()
}
if (cloud2Animator == null) {
binding.cloud2.post {
val cloud2Width: Float =
900f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
cloud2Animator = ObjectAnimator.ofFloat(
binding.cloud2,
"translationX",
cloud2Width - resources.displayMetrics.widthPixels
).apply {
duration = 120_000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
if (cloud2Animator == null) {
binding.cloud2.post {
val cloud2Width: Float =
900f * (resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT)
cloud2Animator = ObjectAnimator.ofFloat(
binding.cloud2,
"translationX",
cloud2Width - resources.displayMetrics.widthPixels
).apply {
duration = 120_000
repeatCount = ValueAnimator.INFINITE
repeatMode = ValueAnimator.REVERSE
start()
}
}
} else if (cloud2Animator?.isPaused == true) {
cloud2Animator?.resume()
}
} else if (cloud2Animator?.isPaused == true) {
cloud2Animator?.resume()
}
handleNightAnimations()
handleNightAnimations()
}
}
private fun handleNightAnimations(){
private fun handleNightAnimations() {
if (currentBackground == TimePeriod.NIGHT && star1Animator == null) {
binding.star1.post {
star1Animator = ObjectAnimator.ofFloat(binding.star1, "alpha", 0.3f, 1f).apply {
@@ -234,7 +270,7 @@ class Home1Fragment : Fragment() {
start()
}
}
} else if (star1Animator?.isPaused == true){
} else if (star1Animator?.isPaused == true) {
star1Animator?.resume()
}
@@ -338,7 +374,9 @@ class Home1Fragment : Fragment() {
binding.moon.show()
currentBackground = timePeriod
handleNightAnimations()
synchronized(this){
handleNightAnimations()
}
binding.grass.setImageResource(R.drawable.img_grass_n)
}

View File

@@ -1,17 +1,22 @@
package com.woka.home.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.woka.R
import com.woka.WokaApp
import com.woka.databinding.FragmentHome2Binding
import com.woka.home.HomeViewModel
import com.woka.mvvm.userDataModels.UserDataResponse
import com.woka.networking.ApiResult
import com.woka.players.LiveStreamPlayerActivity
import com.woka.utils.UserType
import com.woka.utils.toast
@@ -21,6 +26,8 @@ class Home2Fragment : Fragment() {
private lateinit var viewModel: HomeViewModel
private lateinit var player: ExoPlayer
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -29,8 +36,11 @@ class Home2Fragment : Fragment() {
activity?.let {
viewModel = ViewModelProvider(it)[HomeViewModel::class.java]
player = ExoPlayer.Builder(it).build()
}
initPlayerView()
setObservers()
clickEvents()
@@ -38,10 +48,41 @@ class Home2Fragment : Fragment() {
return binding.root
}
override fun onResume() {
super.onResume()
if (!player.isPlaying){
player.play()
}
}
override fun onPause() {
super.onPause()
if (player.isPlaying) player.pause()
}
override fun onDestroy() {
super.onDestroy()
player.release()
}
private fun initPlayerView() {
binding.playerView.player = player
binding.playerView.useController = false
player.volume = 0f
val videoUri = Uri.parse("https://d3volyx7jx7oal.cloudfront.net/master.m3u8")
val mediaItem = MediaItem.fromUri(videoUri)
player.setMediaItem(mediaItem)
player.playWhenReady = true
player.prepare()
}
private fun clickEvents(){
binding.apply {
webSeries.setOnClickListener {
toast("toast")
playerCard.setOnClickListener {
startActivity(Intent(activity, LiveStreamPlayerActivity::class.java))
}
}
}

View File

@@ -0,0 +1,87 @@
package com.woka.players
import android.view.Window
import android.view.WindowManager
import com.jwplayer.pub.api.JWPlayer
import com.jwplayer.pub.api.events.AdCompleteEvent
import com.jwplayer.pub.api.events.AdErrorEvent
import com.jwplayer.pub.api.events.AdPauseEvent
import com.jwplayer.pub.api.events.AdPlayEvent
import com.jwplayer.pub.api.events.AdSkippedEvent
import com.jwplayer.pub.api.events.CompleteEvent
import com.jwplayer.pub.api.events.ErrorEvent
import com.jwplayer.pub.api.events.EventType
import com.jwplayer.pub.api.events.PauseEvent
import com.jwplayer.pub.api.events.PlayEvent
import com.jwplayer.pub.api.events.listeners.AdvertisingEvents.OnAdCompleteListener
import com.jwplayer.pub.api.events.listeners.AdvertisingEvents.OnAdErrorListener
import com.jwplayer.pub.api.events.listeners.AdvertisingEvents.OnAdPauseListener
import com.jwplayer.pub.api.events.listeners.AdvertisingEvents.OnAdPlayListener
import com.jwplayer.pub.api.events.listeners.AdvertisingEvents.OnAdSkippedListener
import com.jwplayer.pub.api.events.listeners.VideoPlayerEvents
import com.jwplayer.pub.api.events.listeners.VideoPlayerEvents.OnPauseListener
import com.jwplayer.pub.api.events.listeners.VideoPlayerEvents.OnPlayListener
class KeepScreenOnHandler(player: JWPlayer, window: Window) : OnPlayListener,
OnPauseListener, VideoPlayerEvents.OnCompleteListener, VideoPlayerEvents.OnErrorListener,
OnAdPlayListener, OnAdPauseListener, OnAdCompleteListener, OnAdSkippedListener,
OnAdErrorListener {
private val mWindow: Window
init {
player.addListener(EventType.PLAY, this)
player.addListener(EventType.PAUSE, this)
player.addListener(EventType.COMPLETE, this)
player.addListener(EventType.ERROR, this)
player.addListener(EventType.AD_PLAY, this)
player.addListener(EventType.AD_PAUSE, this)
player.addListener(EventType.AD_COMPLETE, this)
player.addListener(EventType.AD_ERROR, this)
mWindow = window
}
private fun updateWakeLock(enable: Boolean) {
if (enable) {
mWindow.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
mWindow.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
override fun onError(errorEvent: ErrorEvent) {
updateWakeLock(false)
}
override fun onAdPlay(adPlayEvent: AdPlayEvent) {
updateWakeLock(true)
}
override fun onAdPause(adPauseEvent: AdPauseEvent) {
updateWakeLock(false)
}
override fun onAdComplete(adCompleteEvent: AdCompleteEvent) {
updateWakeLock(false)
}
override fun onAdSkipped(adSkippedEvent: AdSkippedEvent) {
updateWakeLock(false)
}
override fun onAdError(adErrorEvent: AdErrorEvent) {
updateWakeLock(false)
}
override fun onComplete(completeEvent: CompleteEvent) {
updateWakeLock(false)
}
override fun onPause(pauseEvent: PauseEvent) {
updateWakeLock(false)
}
override fun onPlay(playEvent: PlayEvent) {
updateWakeLock(true)
}
}

View File

@@ -0,0 +1,102 @@
package com.woka.players
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.jwplayer.pub.api.JWPlayer
import com.jwplayer.pub.api.configuration.PlayerConfig
import com.jwplayer.pub.api.events.EventType
import com.jwplayer.pub.api.events.listeners.VideoPlayerEvents
import com.jwplayer.pub.api.fullscreen.FullscreenHandler
import com.jwplayer.pub.api.media.playlists.PlaylistItem
import com.woka.databinding.ActivityLiveStreamPlayerBinding
class LiveStreamPlayerActivity : AppCompatActivity(), FullscreenHandler {
companion object{
private const val TAG = "LiveStreamPlayerActivity_tag"
}
private lateinit var binding: ActivityLiveStreamPlayerBinding
private lateinit var player: JWPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityLiveStreamPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
setUpPlayer()
}
override fun onDestroy() {
super.onDestroy()
player.stop()
}
private fun setUpPlayer() {
player = binding.playerView.player
player.setFullscreenHandler(this)
player.setFullscreen(true, false)
player.addListener(EventType.ERROR, VideoPlayerEvents.OnErrorListener {
Log.d(TAG, "onError: ${it.errorCode}")
Log.d(TAG, "onError: ${it.exception}")
Log.d(TAG, "onError: ${it.message}")
})
player.addListener(EventType.SETUP_ERROR, VideoPlayerEvents.OnSetupErrorListener {
Log.d(TAG, "setUpPlayer: $it")
})
// to keep up the screen om when video is being played
KeepScreenOnHandler(player, window)
val playlistItem = PlaylistItem.Builder()
.file("https://d3volyx7jx7oal.cloudfront.net/master.m3u8")
.mediaId("YR5pnlIM")
.build()
val playlist: MutableList<PlaylistItem> = ArrayList()
playlist.add(playlistItem)
val config =
PlayerConfig.Builder()
.playlist(playlist)
.build()
player.setup(config)
player.play()
}
override fun onFullscreenRequested() {}
override fun onFullscreenExitRequested() {
player.stop()
onBackPressedDispatcher.onBackPressed()
}
override fun onAllowRotationChanged(allowRotation: Boolean) {}
override fun updateLayoutParams(layoutParams: ViewGroup.LayoutParams?) {}
override fun setUseFullscreenLayoutFlags(flags: Boolean) {}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".players.LiveStreamPlayerActivity">
<com.jwplayer.pub.view.JWPlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LiveStreamPlayerActivity"/>
</RelativeLayout>

View File

@@ -113,25 +113,22 @@
</LinearLayout>
<androidx.cardview.widget.CardView
android:id="@+id/player_card"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="15dp"
android:layout_marginHorizontal="5dp"
app:cardCornerRadius="10dp"
app:cardBackgroundColor="@color/white"
app:cardBackgroundColor="@color/black"
app:layout_constraintBottom_toTopOf="@id/g1"
app:layout_constraintTop_toBottomOf="@id/profile_image">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_dark"
android:layout_margin="5dp"
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:resize_mode="fill"
/>
</androidx.cardview.widget.CardView>
@@ -141,7 +138,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.35"/>
app:layout_constraintGuide_percent="0.4"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/g2"

View File

@@ -185,8 +185,10 @@
android:translationZ="1dp" />
<ImageView
android:id="@+id/black_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/_28sdp"
android:layout_marginTop="@dimen/_44sdp"
android:layout_marginBottom="@dimen/_10sdp"
@@ -195,12 +197,26 @@
android:contentDescription="@string/image"
android:paddingHorizontal="@dimen/_15sdp"
android:src="@drawable/woka_logo_full"
android:src="@color/black"
android:visibility="visible"
/>
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/_35sdp"
android:layout_marginTop="@dimen/_44sdp"
android:layout_marginBottom="@dimen/_10sdp"
android:background="@color/black"
android:visibility="visible"
/>
</RelativeLayout>
<androidx.constraintlayout.widget.Guideline

View File

@@ -29,5 +29,12 @@
<item name="android:windowTranslucentStatus">true</item>
</style>
<!-- full screen view-->
<style name="FullScreenTheme" parent="Theme.Material3.Light.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
<style name="Theme.Woka" parent="Base.Theme.Woka" />
</resources>

View File

@@ -21,6 +21,7 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.enableJetifier=true
# BASE URLS

View File

@@ -16,6 +16,9 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url 'https://mvn.jwplayer.com/content/repositories/releases/'
}
}
}