-
Notifications
You must be signed in to change notification settings - Fork 397
Refactor: enhance player architecture and fix background playback issue #207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
824fba8
4578d09
9218130
18c3e29
523d751
72b73d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||
| package com.google.jetstream.presentation.screens.videoPlayer | ||||||||||
|
|
||||||||||
| import android.content.Context | ||||||||||
| import androidx.media3.common.C | ||||||||||
| import androidx.media3.common.Player | ||||||||||
| import androidx.media3.common.util.UnstableApi | ||||||||||
| import androidx.media3.datasource.DefaultDataSource | ||||||||||
| import androidx.media3.exoplayer.ExoPlayer | ||||||||||
| import androidx.media3.exoplayer.source.ProgressiveMediaSource | ||||||||||
| import com.google.jetstream.data.entities.MovieDetails | ||||||||||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||||||||||
| import javax.inject.Inject | ||||||||||
|
|
||||||||||
| @UnstableApi | ||||||||||
| // Do not make this a singleton to prevent Released player from Being invoked for play | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * A manager for the video player. | ||||||||||
| * This class is responsible for managing the video playback using the ExoPlayer library. | ||||||||||
| * | ||||||||||
| * @param VideoPlayerManager Creates an Instance of the player and it available across app for Re-use. | ||||||||||
| */ | ||||||||||
| class VideoPlayerManager @Inject constructor( | ||||||||||
| @ApplicationContext private val context: Context | ||||||||||
| ) { | ||||||||||
| private var _exoPlayer: ExoPlayer? = ExoPlayer.Builder(context) | ||||||||||
| .setSeekForwardIncrementMs(10) | ||||||||||
| .setSeekBackIncrementMs(10) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The seek increment values are set to 10 milliseconds, which is too short to be practical for user-controlled seeking. This is likely a typo and should probably be 10 seconds (10,000ms) to provide a better user experience.
Suggested change
|
||||||||||
| .setMediaSourceFactory( | ||||||||||
| ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) | ||||||||||
| ) | ||||||||||
| .setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) | ||||||||||
| .build().apply { | ||||||||||
| playWhenReady = true | ||||||||||
| repeatMode = Player.REPEAT_MODE_OFF | ||||||||||
| } | ||||||||||
|
|
||||||||||
| val player: ExoPlayer | ||||||||||
| get() = _exoPlayer ?: throw IllegalStateException("Player has been released") | ||||||||||
|
|
||||||||||
| fun load(movieDetails: MovieDetails) { | ||||||||||
| player.apply { | ||||||||||
| stop() | ||||||||||
| clearMediaItems() | ||||||||||
| addMediaItem(movieDetails.intoMediaItem()) | ||||||||||
| movieDetails.similarMovies.forEach { addMediaItem(it.intoMediaItem()) } | ||||||||||
| prepare() | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| fun release() { | ||||||||||
| _exoPlayer?.release() | ||||||||||
| _exoPlayer = null | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,39 +16,66 @@ | |
|
|
||
| package com.google.jetstream.presentation.screens.videoPlayer | ||
|
|
||
| import androidx.annotation.OptIn | ||
| import androidx.compose.runtime.Immutable | ||
| import androidx.lifecycle.SavedStateHandle | ||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import androidx.media3.common.util.UnstableApi | ||
| import androidx.media3.exoplayer.ExoPlayer | ||
| import com.google.jetstream.data.entities.MovieDetails | ||
| import com.google.jetstream.data.repositories.MovieRepository | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import javax.inject.Inject | ||
| import kotlinx.coroutines.flow.SharingStarted | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.stateIn | ||
|
|
||
| @UnstableApi | ||
| @HiltViewModel | ||
| class VideoPlayerScreenViewModel @Inject constructor( | ||
| @OptIn(UnstableApi::class) | ||
| class VideoPlayerScreenViewModel | ||
| @Inject constructor( | ||
| savedStateHandle: SavedStateHandle, | ||
| repository: MovieRepository, | ||
| private val repository: MovieRepository, | ||
| private val playerManager: VideoPlayerManager | ||
| ) : ViewModel() { | ||
| val uiState = savedStateHandle | ||
| .getStateFlow<String?>(VideoPlayerScreen.MovieIdBundleKey, null) | ||
|
|
||
| private val movieIdFlow = savedStateHandle.getStateFlow<String?>( | ||
| VideoPlayerScreen.MovieIdBundleKey, | ||
| null | ||
| ) | ||
|
|
||
| val uiState: StateFlow<VideoPlayerScreenUiState> = movieIdFlow | ||
| .map { id -> | ||
| if (id == null) { | ||
| VideoPlayerScreenUiState.Error | ||
| } else { | ||
| val details = repository.getMovieDetails(movieId = id) | ||
| VideoPlayerScreenUiState.Done(movieDetails = details) | ||
| try { | ||
| val details = repository.getMovieDetails(id) | ||
| playerManager.load(details) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling |
||
| VideoPlayerScreenUiState.Done(details) | ||
| } catch (e: Exception) { | ||
| VideoPlayerScreenUiState.Error | ||
| } | ||
|
Comment on lines
+55
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catching a generic try {
val details = repository.getMovieDetails(id)
playerManager.load(details)
VideoPlayerScreenUiState.Done(details)
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
VideoPlayerScreenUiState.Error
} |
||
| } | ||
| }.stateIn( | ||
| } | ||
| .stateIn( | ||
| scope = viewModelScope, | ||
| started = SharingStarted.WhileSubscribed(5_000), | ||
| initialValue = VideoPlayerScreenUiState.Loading | ||
| ) | ||
|
|
||
| val player: ExoPlayer get() = playerManager.player | ||
|
|
||
| override fun onCleared() { | ||
| super.onCleared() | ||
| playerManager.release() | ||
| } | ||
| } | ||
|
|
||
|
|
||
| @Immutable | ||
| sealed class VideoPlayerScreenUiState { | ||
| data object Loading : VideoPlayerScreenUiState() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The KDoc for the class contains an incorrect
@paramtag. The class constructor's parameter iscontext, notVideoPlayerManager. This should be corrected to accurately document the constructor parameter and improve clarity.