From c522e6a7c63018ad8f6abf73ca12cd40953c3268 Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:08:32 +0530 Subject: [PATCH 1/8] Implement enhanced video player with Picture-in-Picture mode and JSON-based video source --- .vscode/settings.json | 3 + app/build.gradle.kts | 34 +++ app/src/main/AndroidManifest.xml | 14 +- app/src/main/assets/video_url.json | 3 + .../pubscale/basicvideoplayer/MainActivity.kt | 44 ++-- .../VideoPlayerApplication.kt | 7 + .../data/api/JsonDataSource.kt | 33 +++ .../basicvideoplayer/data/model/VideoModel.kt | 14 ++ .../data/repository/VideoRepository.kt | 31 +++ .../pubscale/basicvideoplayer/di/AppModule.kt | 46 ++++ .../basicvideoplayer/ui/player/PipHelper.kt | 65 ++++++ .../ui/player/VideoPlayerActivity.kt | 200 ++++++++++++++++++ .../ui/viewmodel/VideoPlayerViewModel.kt | 49 +++++ app/src/main/res/layout/activity_main.xml | 24 ++- .../main/res/layout/activity_video_player.xml | 27 +++ app/src/main/res/values/strings.xml | 3 +- build.gradle.kts | 6 + gradle/libs.versions.toml | 2 + 18 files changed, 569 insertions(+), 36 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/src/main/assets/video_url.json create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt create mode 100644 app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt create mode 100644 app/src/main/res/layout/activity_video_player.xml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19e349a..874bb0c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + id("com.google.dagger.hilt.android") + id("kotlin-kapt") } android { @@ -33,6 +35,13 @@ android { kotlinOptions { jvmTarget = "11" } + buildFeatures { + viewBinding = true + } + // For Hilt + kapt { + correctErrorTypes = true + } } dependencies { @@ -40,6 +49,31 @@ dependencies { implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.exoplayer) + // Hilt for dependency injection + implementation("com.google.dagger:hilt-android:2.50") + implementation(libs.androidx.tracing.perfetto.handshake) + kapt("com.google.dagger:hilt-compiler:2.50") + + // ViewModel and LiveData + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.hilt:hilt-navigation-fragment:1.1.0") + + // Retrofit for network requests + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + // OkHttp for networking + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // Gson for JSON parsing + implementation("com.google.code.gson:gson:2.10.1") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ade8fda..e6ad96b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,11 @@ + + + + android:exported="true" + android:supportsPictureInPicture="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|screenLayout|uiMode"> + + \ No newline at end of file diff --git a/app/src/main/assets/video_url.json b/app/src/main/assets/video_url.json new file mode 100644 index 0000000..a94ad50 --- /dev/null +++ b/app/src/main/assets/video_url.json @@ -0,0 +1,3 @@ +{ + "url": "https://storage.googleapis.com/test-gg-1/Sample%20Video.mp4" +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt b/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt index 1de2e04..99830d5 100644 --- a/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt +++ b/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt @@ -1,40 +1,28 @@ package com.pubscale.basicvideoplayer -import android.net.Uri +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.media3.common.MediaItem -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.ui.PlayerView +import com.pubscale.basicvideoplayer.ui.player.VideoPlayerActivity +import dagger.hilt.android.AndroidEntryPoint +/** + * Entry point of the application. + * Simply launches the VideoPlayerActivity and finishes. + */ +@AndroidEntryPoint class MainActivity : AppCompatActivity() { - private var player: ExoPlayer? = null - private var playerView: PlayerView? = null - + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - setupExoPLayer() - } - - private fun setupExoPLayer() { - playerView = findViewById(R.id.player_view) - player = ExoPlayer.Builder(this).build() - playerView?.player = player - val videoUri = Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_video) - val mediaItem = MediaItem.fromUri(videoUri) - player?.setMediaItem(mediaItem) - player?.prepare() - player?.playWhenReady = true + startVideoPlayer() } - - override fun onRestart() { - super.onRestart() - player?.play() - } - - override fun onStop() { - super.onStop() - player?.pause() + + // Launch the video player activity + private fun startVideoPlayer() { + val intent = Intent(this, VideoPlayerActivity::class.java) + startActivity(intent) + finish() } } \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt b/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt new file mode 100644 index 0000000..bd5991e --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt @@ -0,0 +1,7 @@ +package com.pubscale.basicvideoplayer + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class VideoPlayerApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt new file mode 100644 index 0000000..e582f39 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt @@ -0,0 +1,33 @@ +package com.pubscale.basicvideoplayer.data.api + +import android.content.Context +import com.google.gson.Gson +import com.pubscale.basicvideoplayer.data.model.VideoResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * Data source for reading video information from the JSON file. + * Uses app context to access assets folder. + */ +class JsonDataSource @Inject constructor( + private val context: Context, + private val gson: Gson +) { + + companion object { + private const val JSON_FILE_NAME = "video_url.json" + } + + /** + * Reads the JSON file from assets and converts it to VideoResponse. + * Uses IO dispatcher to avoid blocking the main thread. + */ + suspend fun getVideoUrl(): VideoResponse { + return withContext(Dispatchers.IO) { + val jsonString = context.assets.open(JSON_FILE_NAME).bufferedReader().use { it.readText() } + gson.fromJson(jsonString, VideoResponse::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt new file mode 100644 index 0000000..b6d7198 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt @@ -0,0 +1,14 @@ +package com.pubscale.basicvideoplayer.data.model + +/** + * Data class that represents the video information from JSON. + * Contains the URL of the video to be played. + */ +data class VideoResponse( + val url: String +) { + companion object { + // Fallback URL to use if JSON reading fails or returns empty URL + const val FALLBACK_VIDEO_URL = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt new file mode 100644 index 0000000..1da1784 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt @@ -0,0 +1,31 @@ +package com.pubscale.basicvideoplayer.data.repository + +import com.pubscale.basicvideoplayer.data.api.JsonDataSource +import com.pubscale.basicvideoplayer.data.model.VideoResponse +import javax.inject.Inject + +/** + * Repository that provides video URL data. + * Handles fetching from JSON and provides fallback if needed. + */ +class VideoRepository @Inject constructor( + private val jsonDataSource: JsonDataSource +) { + + /** + * Gets the video URL from local JSON file or returns fallback URL. + * Ensures a valid URL is always returned. + */ + suspend fun getVideoUrl(): String { + return try { + val response = jsonDataSource.getVideoUrl() + + // Return URL if valid, otherwise use fallback + response.url.takeIf { it.isNotEmpty() } + ?: VideoResponse.FALLBACK_VIDEO_URL + } catch (e: Exception) { + // On any error, return the fallback URL + VideoResponse.FALLBACK_VIDEO_URL + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt b/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt new file mode 100644 index 0000000..0a48ab2 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt @@ -0,0 +1,46 @@ +package com.pubscale.basicvideoplayer.di + +import android.content.Context +import com.google.gson.Gson +import com.pubscale.basicvideoplayer.data.api.JsonDataSource +import com.pubscale.basicvideoplayer.data.repository.VideoRepository +import com.pubscale.basicvideoplayer.ui.player.PipHelper +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Hilt module that provides dependencies for the application. + * All dependencies are provided as singletons. + */ +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + // Provides the PiP helper + @Provides + @Singleton + fun providePipHelper() = PipHelper() + + // Provides Gson for JSON parsing + @Provides + @Singleton + fun provideGson() = Gson() + + // Provides the data source for JSON operations + @Provides + @Singleton + fun provideJsonDataSource( + @ApplicationContext context: Context, + gson: Gson + ) = JsonDataSource(context, gson) + + // Provides the repository to access video data + @Provides + @Singleton + fun provideVideoRepository(jsonDataSource: JsonDataSource) = + VideoRepository(jsonDataSource) +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt new file mode 100644 index 0000000..3cc2127 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt @@ -0,0 +1,65 @@ +package com.pubscale.basicvideoplayer.ui.player + +import android.app.Activity +import android.app.PictureInPictureParams +import android.os.Build +import android.util.Rational +import android.view.View +import androidx.media3.common.Player +import javax.inject.Inject + +/** + * Helper class to handle Picture-in-Picture mode functionality. + * Centralizes PiP logic to keep the activity code clean. + */ +class PipHelper @Inject constructor() { + + /** + * Attempts to enter PiP mode if the device supports it. + * + * @param activity The activity that should enter PiP mode + * @param playerView The view showing the video + * @param player The ExoPlayer instance + * @return wasPlaying True if the player was playing when PiP mode was entered + */ + fun enterPipMode(activity: Activity, playerView: View?, player: Player?): Boolean { + val isVideoReady = player?.playbackState == Player.STATE_READY + val wasPlaying = player?.isPlaying == true + + if (isVideoReady && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Set 16:9 aspect ratio if we can't get the actual dimensions + val aspectRatio = Rational(playerView?.width ?: 16, playerView?.height ?: 9) + + val pipParamsBuilder = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + + activity.enterPictureInPictureMode(pipParamsBuilder.build()) + return wasPlaying + } + + return false + } + + /** + * Handles UI and playback changes when PiP mode changes. + * Shows/hides controls and manages playback state. + */ + fun handlePipModeChanged( + isInPipMode: Boolean, + player: Player?, + showControls: (Boolean) -> Unit, + wasPlaying: Boolean + ) { + // Hide player controls in PiP mode + showControls(!isInPipMode) + + // When exiting PiP mode, restore the previous playback state + if (!isInPipMode) { + if (wasPlaying) { + player?.play() + } else { + player?.pause() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt new file mode 100644 index 0000000..3c05744 --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt @@ -0,0 +1,200 @@ +package com.pubscale.basicvideoplayer.ui.player + +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.ProgressBar +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import com.pubscale.basicvideoplayer.R +import com.pubscale.basicvideoplayer.ui.viewmodel.VideoPlayerViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +/** + * Main activity that handles video playback and PiP functionality. + * Observes the ViewModel to get video URL and manages player lifecycle. + */ +@AndroidEntryPoint +class VideoPlayerActivity : AppCompatActivity() { + private var player: ExoPlayer? = null + private var playerView: PlayerView? = null + private lateinit var progressBar: ProgressBar + private val viewModel: VideoPlayerViewModel by viewModels() + private var isVideoReady = false + private var wasPlayingBeforePip = false + + @Inject + lateinit var pipHelper: PipHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_video_player) + + playerView = findViewById(R.id.player_view) + progressBar = findViewById(R.id.progress_bar) + + setupExoPlayer() + setupObservers() + } + + // Connect to ViewModel and observe changes + private fun setupObservers() { + viewModel.videoUrl.observe(this) { url -> + if (url.isNotEmpty()) { + loadRemoteVideo(url) + } + } + + viewModel.isLoading.observe(this) { isLoading -> + progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + } + } + + // Initialize the ExoPlayer instance + private fun setupExoPlayer() { + player = ExoPlayer.Builder(this).build().apply { + addListener(PlayerEventListener()) + } + + playerView?.player = player + player?.playWhenReady = true + } + + // Listen for player events to update UI and handle errors + private inner class PlayerEventListener : Player.Listener { + override fun onPlayerError(error: PlaybackException) { + loadLocalFallbackVideo() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> { + isVideoReady = true + progressBar.visibility = View.GONE + } + Player.STATE_BUFFERING -> { + progressBar.visibility = View.VISIBLE + } + } + } + } + + // Load video from the URL provided by the ViewModel + private fun loadRemoteVideo(videoUrl: String) { + progressBar.visibility = View.VISIBLE + try { + val uri = Uri.parse(videoUrl) + val mediaItem = MediaItem.fromUri(uri) + + player?.setMediaItem(mediaItem) + player?.prepare() + + player?.addListener(object : Player.Listener { + override fun onPlayerError(error: PlaybackException) { + player?.removeListener(this) + loadLocalFallbackVideo() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + if (playbackState == Player.STATE_READY) { + progressBar.visibility = View.GONE + player?.play() + player?.removeListener(this) + } + } + }) + } catch (e: Exception) { + loadLocalFallbackVideo() + } + } + + // Fallback to local video if remote loading fails + private fun loadLocalFallbackVideo() { + val videoUri = Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_video) + val mediaItem = MediaItem.fromUri(videoUri) + player?.setMediaItem(mediaItem) + player?.prepare() + player?.play() + } + + // Enter PiP mode when user navigates away from the app + override fun onUserLeaveHint() { + super.onUserLeaveHint() + wasPlayingBeforePip = player?.isPlaying == true + pipHelper.enterPipMode(this, playerView, player) + } + + // Handle PiP mode changes - show/hide controls and manage playback + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration + ) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + + pipHelper.handlePipModeChanged( + isInPictureInPictureMode, + player, + { showControls -> playerView?.useController = showControls }, + wasPlayingBeforePip + ) + } + + // Enter PiP mode when back button is pressed instead of exiting + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + if (isVideoReady) { + wasPlayingBeforePip = player?.isPlaying == true + pipHelper.enterPipMode(this, playerView, player) + } else { + super.onBackPressed() + } + } + + // Lifecycle management for ExoPlayer + override fun onStart() { + super.onStart() + if (player == null) { + setupExoPlayer() + } + } + + override fun onRestart() { + super.onRestart() + player?.play() + } + + override fun onPause() { + super.onPause() + if (!isInPictureInPictureMode) { + player?.pause() + } + } + + override fun onResume() { + super.onResume() + if (!isInPictureInPictureMode) { + player?.play() + } + } + + override fun onStop() { + super.onStop() + } + + override fun onDestroy() { + super.onDestroy() + releasePlayer() + } + + private fun releasePlayer() { + player?.release() + player = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt new file mode 100644 index 0000000..9027a7a --- /dev/null +++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt @@ -0,0 +1,49 @@ +package com.pubscale.basicvideoplayer.ui.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pubscale.basicvideoplayer.data.repository.VideoRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * ViewModel for the video player screen. + * Fetches video URL from repository and exposes it to the UI. + */ +@HiltViewModel +class VideoPlayerViewModel @Inject constructor( + private val repository: VideoRepository +) : ViewModel() { + + // Holds the video URL to be played + private val _videoUrl = MutableLiveData() + val videoUrl: LiveData = _videoUrl + + // Indicates whether loading is in progress + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + init { + fetchVideoUrl() + } + + // Fetch video URL from the repository + fun fetchVideoUrl() { + viewModelScope.launch { + try { + _isLoading.value = true + + val url = repository.getVideoUrl() + _videoUrl.value = url + + } catch (e: Exception) { + // Repository should handle all errors, but just in case + } finally { + _isLoading.value = false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 32f41fe..8376158 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,11 +7,23 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_video_player.xml b/app/src/main/res/layout/activity_video_player.xml new file mode 100644 index 0000000..51fcde8 --- /dev/null +++ b/app/src/main/res/layout/activity_video_player.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12edb2d..c5b9ce8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ - BasicVideoPlayer + Basic Video Player + Loading Video Player \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 922f551..cac1431 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,10 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false +} + +buildscript { + dependencies { + classpath("com.google.dagger:hilt-android-gradle-plugin:2.50") + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4e12e3..603349e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ activity = "1.10.0" constraintlayout = "2.2.0" media3Ui = "1.5.1" media3Exoplayer = "1.5.1" +tracingPerfettoHandshake = "1.0.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -23,6 +24,7 @@ material = { group = "com.google.android.material", name = "material", version.r androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3Exoplayer" } +androidx-tracing-perfetto-handshake = { group = "androidx.tracing", name = "tracing-perfetto-handshake", version.ref = "tracingPerfettoHandshake" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 8f078311a9ca21e12c54c20557cc5b09b6bdddb1 Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:18:50 +0530 Subject: [PATCH 2/8] Create README.md --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..00828aa --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Basic Video Player with PiP + +A simple video player Android application that demonstrates proper MVVM architecture implementation with Picture-in-Picture (PiP) functionality and dynamic video source loading from a JSON file. + +## Features + +- **Picture-in-Picture Mode**: Continue watching videos in a small floating window when you navigate away from the app +- **Dynamic Video Source**: Video URL is loaded from a local JSON file +- **MVVM Architecture**: Clean separation of concerns with Model-View-ViewModel pattern +- **Error Handling**: Graceful fallback to local video when remote loading fails +- **Dependency Injection**: Uses Hilt for clean dependency management + +## Screenshots + +| Full Screen Playback | Picture-in-Picture Mode | +|:---:|:---:| +| ![Full Screen](https://github.com/user-attachments/assets/5dffb438-651b-4462-93a7-4fc94e81ab86) | ![PiP Mode](https://github.com/user-attachments/assets/3449e856-b31c-4ab1-a8f1-890a33e78fd2) | + +## Implementation Details + +### Architecture Components + +- **Model**: Handles data operations through JsonDataSource and VideoRepository +- **View**: UI components in VideoPlayerActivity with lifecycle-aware playback +- **ViewModel**: Manages business logic and UI state in VideoPlayerViewModel + +### Libraries Used + +- ExoPlayer for video playback +- Hilt for dependency injection +- Gson for JSON parsing +- Android Lifecycle components for MVVM implementation +- Kotlin Coroutines for asynchronous operations + +## Getting Started + +1. Clone the repository +2. Open the project in Android Studio +3. Run the app on an emulator or physical device running Android 8.0 (API level 26) or higher for full PiP support + +## How It Works + +The app reads a video URL from a JSON file in the assets folder and plays it using ExoPlayer. When you press back or navigate away from the app, it automatically enters Picture-in-Picture mode, allowing you to continue watching the video while using other apps. + +If there's any issue loading the video from the remote URL, the app will automatically fall back to a local video resource. + +## Requirements + +- Android 8.0 (API level 26) or higher for PiP functionality +- Android Studio Arctic Fox or newer From 5d41a7d496c0f9bb7da93f156c140868d218ce9a Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:20:40 +0530 Subject: [PATCH 3/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00828aa..128744e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A simple video player Android application that demonstrates proper MVVM architec | Full Screen Playback | Picture-in-Picture Mode | |:---:|:---:| -| ![Full Screen](https://github.com/user-attachments/assets/5dffb438-651b-4462-93a7-4fc94e81ab86) | ![PiP Mode](https://github.com/user-attachments/assets/3449e856-b31c-4ab1-a8f1-890a33e78fd2) | +| | | ## Implementation Details From 076920e1ef04c7af95861e680d447c4df5b5ca4e Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:22:32 +0530 Subject: [PATCH 4/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 128744e..3620d24 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A simple video player Android application that demonstrates proper MVVM architec | Full Screen Playback | Picture-in-Picture Mode | |:---:|:---:| -| | | +| ![Full Screen](https://i.imgur.com/z71HVMZ.jpg) | ![PiP Mode](https://i.imgur.com/Xzue8Fq.jpg) | ## Implementation Details From dfec0b205863e93e6ec08d7cfae015bb9c358f17 Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:24:44 +0530 Subject: [PATCH 5/8] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3620d24..3932db0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,10 @@ A simple video player Android application that demonstrates proper MVVM architec | Full Screen Playback | Picture-in-Picture Mode | |:---:|:---:| -| ![Full Screen](https://i.imgur.com/z71HVMZ.jpg) | ![PiP Mode](https://i.imgur.com/Xzue8Fq.jpg) | +full screen- ![Screenshot_20250509_111342](https://github.com/user-attachments/assets/7dc2418d-8553-48ae-a404-f95c4aadf805) +pip- +![Screenshot_20250509_111413](https://github.com/user-attachments/assets/06dd6619-01e4-4b90-b65b-d796e4288241) + ## Implementation Details From 79dbad8fc33c31e09a1c45645e85f86291ff0480 Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 11:25:32 +0530 Subject: [PATCH 6/8] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3932db0..3b5e050 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,12 @@ A simple video player Android application that demonstrates proper MVVM architec - **MVVM Architecture**: Clean separation of concerns with Model-View-ViewModel pattern - **Error Handling**: Graceful fallback to local video when remote loading fails - **Dependency Injection**: Uses Hilt for clean dependency management - ## Screenshots | Full Screen Playback | Picture-in-Picture Mode | |:---:|:---:| -full screen- ![Screenshot_20250509_111342](https://github.com/user-attachments/assets/7dc2418d-8553-48ae-a404-f95c4aadf805) -pip- -![Screenshot_20250509_111413](https://github.com/user-attachments/assets/06dd6619-01e4-4b90-b65b-d796e4288241) +| | | + ## Implementation Details From 0fbcb4d269a357b98b9751d1fb7ffeeecacbb8b4 Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 12:12:52 +0530 Subject: [PATCH 7/8] Update README.md --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 3b5e050..e217d02 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,37 @@ A simple video player Android application that demonstrates proper MVVM architec ## How It Works +## How It Works + The app reads a video URL from a JSON file in the assets folder and plays it using ExoPlayer. When you press back or navigate away from the app, it automatically enters Picture-in-Picture mode, allowing you to continue watching the video while using other apps. If there's any issue loading the video from the remote URL, the app will automatically fall back to a local video resource. +### Architecture Flow + +1. **Data Loading**: When the app starts, the `JsonDataSource` reads the video URL from the assets folder and passes it to the `VideoRepository`. + +2. **Repository Layer**: The `VideoRepository` processes the data and handles any potential errors, ensuring a valid URL is always provided. + +3. **ViewModel Processing**: The `VideoPlayerViewModel` requests the URL from the repository and exposes it to the UI through LiveData, along with loading states. + +4. **UI Rendering**: The `VideoPlayerActivity` observes the ViewModel's LiveData and updates the UI accordingly, loading the video when the URL is available. + +5. **Video Playback**: ExoPlayer handles the actual video playback, with proper lifecycle management to prevent memory leaks. + +6. **PiP Functionality**: The `PipHelper` class manages transitions to and from Picture-in-Picture mode, maintaining the playback state during transitions. + +### Error Handling + +The app implements multiple layers of error handling to ensure a smooth user experience: + +- JSON parsing errors are caught in the data source +- Network errors are handled when loading the video +- Playback errors trigger fallback to local video +- UI states reflect loading progress and success states + +This multi-layered approach ensures that users always have a working video player experience, even when offline or when the remote video source is unavailable. + ## Requirements - Android 8.0 (API level 26) or higher for PiP functionality From 4a7b046a50ffd03569285e3ad67ff27b3e7d78eb Mon Sep 17 00:00:00 2001 From: Ujjwal sai Date: Fri, 9 May 2025 12:13:09 +0530 Subject: [PATCH 8/8] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index e217d02..a3c8159 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,6 @@ A simple video player Android application that demonstrates proper MVVM architec ## How It Works -## How It Works - The app reads a video URL from a JSON file in the assets folder and plays it using ExoPlayer. When you press back or navigate away from the app, it automatically enters Picture-in-Picture mode, allowing you to continue watching the video while using other apps. If there's any issue loading the video from the remote URL, the app will automatically fall back to a local video resource.