Created
June 11, 2021 11:33
-
-
Save n0m0r3pa1n/d3484dce93464433e81065767d516619 to your computer and use it in GitHub Desktop.
Sample player that supports both cast & simple exo player
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.android.exoplayer2.* | |
import com.google.android.exoplayer2.audio.AudioAttributes | |
import com.google.android.exoplayer2.audio.AudioListener | |
import com.google.android.exoplayer2.ext.cast.CastPlayer | |
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener | |
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource | |
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory | |
import com.google.android.exoplayer2.source.ProgressiveMediaSource | |
import com.google.android.exoplayer2.upstream.FileDataSource | |
import okhttp3.OkHttpClient | |
import javax.inject.Inject | |
class MyCustomPlayerPlayer @Inject constructor( | |
private val context: Context, | |
private val castPlayerManager: CastPlayerManager, | |
private val mediaItemMapper: MediaItemMapper, | |
private val mediaQueueItemMapper: MediaQueueItemMapper | |
) { | |
private val mediaItemsCastQueue = mutableSetOf<MediaItem>() | |
private val onMediaItemTransitionListener = object : Player.EventListener { | |
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { | |
lastWindowIndexId = currentPlayer.currentWindowIndex | |
} | |
} | |
private val currentAudioComponent: Player.AudioComponent? | |
get() = currentPlayer.audioComponent | |
private val castPlayer: CastPlayer? | |
get() = castPlayerManager.getCastPlayer() | |
private var lastWindowIndexId: Int = 0 | |
private val playerEventListener = PlayerEventListener() | |
private val fileDataSourceFactory = ProgressiveMediaSource.Factory(FileDataSource.Factory()) | |
private lateinit var simpleExoPlayer: SimpleExoPlayer | |
private val chromecastSessionSelectedListener = object : SessionAvailabilityListener { | |
override fun onCastSessionAvailable() { | |
if (castPlayer != null) { | |
mediaItemsCastQueue.addAll(simpleExoPlayer.getMediaItems()) | |
setCurrentPlayer(castPlayer!!) | |
} | |
} | |
override fun onCastSessionUnavailable() { | |
setCurrentPlayer(simpleExoPlayer) | |
} | |
} | |
private lateinit var currentPlayer: Player | |
val currentPosition | |
get() = currentPlayer.currentPosition | |
val mediaItemCount | |
get() = currentPlayer.mediaItemCount | |
val shouldPersistState | |
get() = currentPlayer.shouldPersistState | |
val isPlaying | |
get() = currentPlayer.isPlaying | |
val duration | |
get() = currentPlayer.duration | |
val playbackParameters | |
get() = currentPlayer.playbackParameters | |
val currentWindowIndex | |
get() = currentPlayer.currentWindowIndex | |
override val playbackState | |
get() = currentPlayer.playbackState | |
override val isCasting | |
get() = currentPlayer is CastPlayer || castPlayerManager.isCasting() | |
override val bufferedPercentage: Int | |
get() = currentPlayer.bufferedPercentage | |
fun init() { | |
simpleExoPlayer = createExoplayer() | |
currentPlayer = if (castPlayer?.isCastSessionAvailable == true) castPlayer!! else simpleExoPlayer | |
addListener(playerEventListener) | |
addListener(onMediaItemTransitionListener) | |
addAudioListener(playerEventListener) | |
castPlayerManager.setAvailabilityListener(chromecastSessionSelectedListener) | |
} | |
private fun createExoplayer(): SimpleExoPlayer { | |
val audioStreamInfo = AudioAttributes.Builder() | |
.setContentType(C.CONTENT_TYPE_SPEECH) | |
.setUsage(C.USAGE_MEDIA) | |
.build() | |
val okHttpClient = OkHttpClient.Builder().build() | |
return SimpleExoPlayer.Builder(context) | |
.setAudioAttributes(audioStreamInfo, true) | |
.setMediaSourceFactory(DefaultMediaSourceFactory(OkHttpDataSource.Factory(okHttpClient))) | |
.build() | |
} | |
fun getCurrentPlayer() = currentPlayer | |
fun clearMediaItems() { | |
mediaItemsCastQueue.clear() | |
currentPlayer.clearMediaItems() | |
} | |
fun setEpisodes(episodesList: List<Episode>, startIndex: Int, positionMs: Long) { | |
if (episodesList.isNullOrEmpty()) return | |
mediaItemsCastQueue.clear() | |
currentPlayer.clearMediaItems() | |
if (isCasting) { | |
mediaItemsCastQueue.addAll( | |
// Casting for files is not possible. Google Cast does not work | |
// https://stackoverflow.com/questions/32049851/it-is-posible-to-cast-or-stream-android-chromecast-a-local-file | |
episodesList | |
.filter { it.streamUrl?.startsWith("http") == true } | |
.map { mediaItemMapper.toMediaItem(it) } | |
) | |
playerEventListener.onMediaItemTransition( | |
mediaItemsCastQueue.elementAt(startIndex), | |
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK | |
) | |
setupMediaItemsForCast(startIndex, positionMs) | |
} else { | |
setEpisodesForExoPlayer(episodesList) | |
currentPlayer.seekTo(startIndex, positionMs) | |
} | |
} | |
private fun setupMediaItemsForCast(startIndex: Int, positionMs: Long) { | |
castPlayer!!.loadItems( | |
mediaItemsCastQueue.map { mediaQueueItemMapper.toMediaQueueItem(it) }.toTypedArray(), | |
startIndex, positionMs, Player.REPEAT_MODE_OFF | |
) | |
} | |
private fun setEpisodesForExoPlayer(episodesList: List<Episode>) { | |
episodesList.forEach { episode -> | |
if (episode.streamUrl!!.startsWith("http")) { | |
val mediaItem = mediaItemMapper.toMediaItem(episode) | |
mediaItemsCastQueue.add(mediaItem) | |
currentPlayer.addMediaItem(mediaItem) | |
} else { | |
(currentPlayer as SimpleExoPlayer).addMediaSource( | |
fileDataSourceFactory.createMediaSource(mediaItemMapper.toMediaItem(episode)) | |
) | |
} | |
} | |
} | |
fun addListener(listener: Player.EventListener) { | |
currentPlayer.addListener(listener) | |
} | |
private fun addAudioListener(listener: AudioListener) { | |
currentAudioComponent?.addAudioListener(listener) | |
} | |
fun addEventListener(listener: PlayerListenerInterface) { | |
playerEventListener.addListener(listener) | |
} | |
fun removeEventListener(listener: PlayerListenerInterface) { | |
playerEventListener.removeListener(listener) | |
} | |
fun getMediaItemAt(index: Int): MediaItem { | |
return currentPlayer.getMediaItemAt(index) | |
} | |
fun seekTo(positionMs: Long) { | |
currentPlayer.seekTo(positionMs) | |
} | |
fun prepare() { | |
currentPlayer.prepare() | |
} | |
fun play() { | |
currentPlayer.play() | |
} | |
fun pause() { | |
currentPlayer.pause() | |
} | |
fun previous() { | |
currentPlayer.previous() | |
} | |
fun next() { | |
currentPlayer.next() | |
} | |
fun hasNext() = currentPlayer.hasNext() | |
fun hasPrevious() = currentPlayer.hasPrevious() | |
fun setPlayWhenReady(playWhenReady: Boolean) { | |
currentPlayer.playWhenReady = playWhenReady | |
} | |
fun stop() { | |
if (::currentPlayer.isInitialized) { | |
currentPlayer.stop() | |
} | |
} | |
fun setPlaybackParameters(playbackParameters: PlaybackParameters) { | |
currentPlayer.setPlaybackParameters(playbackParameters) | |
} | |
fun release() { | |
mediaItemsCastQueue.clear() | |
castPlayerManager.release() | |
playerEventListener.removeAllListeners() | |
if (::currentPlayer.isInitialized) { | |
currentPlayer.removeListener(playerEventListener) | |
currentPlayer.removeListener(onMediaItemTransitionListener) | |
currentAudioComponent?.removeAudioListener(playerEventListener) | |
currentPlayer.stop() | |
currentPlayer.release() | |
} | |
} | |
private fun setCurrentPlayer(newPlayer: Player) { | |
if (this.currentPlayer === newPlayer) { | |
return | |
} | |
val previousPlayer = this.currentPlayer | |
val previousPlayerState = previousPlayer.getCurrentPlayerState() | |
previousPlayer.stop() | |
currentPlayer = newPlayer | |
currentPlayer.setPlayerState(previousPlayerState) | |
addListener(playerEventListener) | |
addAudioListener(playerEventListener) | |
previousPlayer.removeListener(playerEventListener) | |
previousPlayer.audioComponent?.removeAudioListener(playerEventListener) | |
} | |
private fun Player.setPlayerState(playerState: PlayerState) { | |
setMediaItems(mediaItemsCastQueue.toList(), playerState.currentWindowIndex, playerState.currentPosition) | |
playWhenReady = playerState.playWhenReady | |
prepare() | |
} | |
private fun Player.getCurrentPlayerState() = PlayerState( | |
currentPosition = if (playbackState != Player.STATE_ENDED) currentPosition else C.TIME_UNSET, | |
currentWindowIndex = if (playbackState != Player.STATE_ENDED) lastWindowIndexId else C.INDEX_UNSET, | |
playbackState = playbackState, | |
playWhenReady = if (playbackState != Player.STATE_ENDED) playWhenReady else false | |
) | |
private fun Player.getMediaItems(): List<MediaItem> { | |
return List(mediaItemCount) { getMediaItemAt(it) } | |
} | |
private val Player.shouldPersistState | |
get() = playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED | |
fun getCurrentMediaItemId(): String? { | |
val currentMediaIndex = currentPlayer.currentWindowIndex | |
lastWindowIndexId = currentMediaIndex | |
if (currentPlayer is CastPlayer) { | |
val queueSize = mediaItemsCastQueue.size | |
return mediaItemsCastQueue.takeIf { queueSize > currentMediaIndex }?.elementAt(currentMediaIndex)?.mediaId | |
} else { | |
return currentPlayer.currentMediaItem?.mediaId | |
} | |
} | |
data class PlayerState( | |
val currentPosition: Long, | |
val currentWindowIndex: Int, | |
val playbackState: Int? = null, | |
val playWhenReady: Boolean | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment