First early test of ExoPlayer

Currently only plays some movies
This commit is contained in:
Jarne Demeulemeester 2021-07-07 21:19:47 +02:00
parent 4bbf40bc22
commit fcbd7d1f33
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
10 changed files with 248 additions and 4 deletions

View file

@ -87,6 +87,12 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// ExoPlayer
def exoplayer_version = "2.14.1"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View file

@ -12,13 +12,14 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Jellyfin">
<activity android:name=".PlayerActivity"></activity>
<activity
android:name=".MainActivity"
android:label="@string/title_activity_main" />
<activity
android:name=".SetupActivity"
android:windowSoftInputMode="adjustPan"
android:theme="@style/Theme.JellyfinSplashScreen">
android:theme="@style/Theme.JellyfinSplashScreen"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -0,0 +1,47 @@
package dev.jdtech.jellyfin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.navigation.navArgs
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.util.MimeTypes
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
@AndroidEntryPoint
class PlayerActivity : AppCompatActivity() {
private val viewModel: PlayerActivityViewModel by viewModels()
private val args: PlayerActivityArgs by navArgs()
private lateinit var playerView: PlayerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("PlayerActivity", "onCreate")
setContentView(R.layout.activity_player)
playerView = findViewById(R.id.video_view)
viewModel.player.observe(this, {
playerView.player = it
})
if (viewModel.player.value == null) {
viewModel.initializePlayer(args.itemId)
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("PlayerActivity", "onDestroy")
}
}

View file

@ -32,6 +32,8 @@ class JellyfinApi(context: Context, baseUrl: String) {
val userLibraryApi = UserLibraryApi(api)
val showsApi = TvShowsApi(api)
val sessionApi = SessionApi(api)
val videosApi = VideosApi(api)
val mediaInfoApi = MediaInfoApi(api)
companion object {
@Volatile

View file

@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.*
@AndroidEntryPoint
class MediaInfoFragment : Fragment() {
@ -70,6 +71,10 @@ class MediaInfoFragment : Fragment() {
}, fixedWidth = true)
binding.peopleRecyclerView.adapter = PersonListAdapter()
binding.playButton.setOnClickListener {
navigateToPlayerActivity(args.itemId)
}
viewModel.loadData(args.itemId)
}
@ -91,4 +96,10 @@ class MediaInfoFragment : Fragment() {
)
)
}
private fun navigateToPlayerActivity(itemId: UUID) {
findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(itemId)
)
}
}

View file

@ -14,4 +14,6 @@ interface JellyfinRepository {
suspend fun getNextUp(seriesId: UUID): List<BaseItemDto>
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID, fields: List<ItemFields>? = null): List<BaseItemDto>
suspend fun getStreamUrl(itemId: UUID): String
}

View file

@ -3,8 +3,7 @@ package dev.jdtech.jellyfin.repository
import dev.jdtech.jellyfin.api.JellyfinApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.*
import java.util.*
class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRepository {
@ -60,4 +59,49 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
}
return episodes
}
override suspend fun getStreamUrl(itemId: UUID): String {
val streamUrl: String
withContext(Dispatchers.IO) {
/*val mediaInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
itemId, PlaybackInfoDto(
userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = listOf(),
containerProfiles = listOf(),
directPlayProfiles = listOf(
DirectPlayProfile(
type = DlnaProfileType.VIDEO
), DirectPlayProfile(type = DlnaProfileType.AUDIO)
),
transcodingProfiles = listOf(),
responseProfiles = listOf(),
enableAlbumArtInDidl = false,
enableMsMediaReceiverRegistrar = false,
enableSingleAlbumArtLimit = false,
enableSingleSubtitleLimit = false,
ignoreTranscodeByteRangeRequests = false,
maxAlbumArtHeight = 1_000_000_000,
maxAlbumArtWidth = 1_000_000_000,
requiresPlainFolders = false,
requiresPlainVideoItems = false,
timelineOffsetSeconds = 0
),
startTimeTicks = null,
audioStreamIndex = null,
subtitleStreamIndex = null,
maxStreamingBitrate = 1_000_000_000,
)
).content*/
streamUrl = jellyfinApi.videosApi.getVideoStreamUrl(
itemId,
static = true,
mediaSourceId = itemId.toString()
)
}
return streamUrl
}
}

View file

@ -0,0 +1,106 @@
package dev.jdtech.jellyfin.viewmodels
import android.app.Application
import android.content.Context
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.util.MimeTypes
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject
@HiltViewModel
class PlayerActivityViewModel
@Inject
constructor(
private val application: Application,
private val jellyfinRepository: JellyfinRepository
) : ViewModel() {
private var _player = MutableLiveData<SimpleExoPlayer>()
var player: LiveData<SimpleExoPlayer> = _player
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition: Long = 0
private var playbackStateListener: PlaybackStateListener
init {
playbackStateListener = PlaybackStateListener()
}
fun initializePlayer(itemId: UUID) {
if (player.value == null) {
val trackSelector = DefaultTrackSelector(application)
trackSelector.parameters.buildUpon().setMaxVideoSizeSd()
_player.value = SimpleExoPlayer.Builder(application)
.setTrackSelector(trackSelector)
.build()
}
player.value?.addListener(playbackStateListener)
viewModelScope.launch {
val streamUrl = jellyfinRepository.getStreamUrl(itemId)
val mediaItem =
MediaItem.Builder()
.setMediaId(itemId.toString())
.setUri(streamUrl)
.build()
player.value?.setMediaItem(mediaItem)
}
player.value?.playWhenReady = playWhenReady
player.value?.seekTo(currentWindow, playbackPosition)
player.value?.prepare()
}
fun releasePlayer() {
if (player.value != null) {
playWhenReady = player.value!!.playWhenReady
playbackPosition = player.value!!.currentPosition
currentWindow = player.value!!.currentWindowIndex
player.value!!.removeListener(playbackStateListener)
player.value!!.release()
_player.value = null
}
}
class PlaybackStateListener : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
var stateString = "UNKNOWN_STATE -"
when (state) {
ExoPlayer.STATE_IDLE -> {
stateString = "ExoPlayer.STATE_IDLE -";
}
ExoPlayer.STATE_BUFFERING -> {
stateString = "ExoPlayer.STATE_BUFFERING -";
}
ExoPlayer.STATE_READY -> {
stateString = "ExoPlayer.STATE_READY -";
}
ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -";
}
}
Log.d("PlayerActivity", "changed state to $stateString")
}
}
override fun onCleared() {
super.onCleared()
Log.d("PlayerActivity", "onCleared ViewModel")
releasePlayer()
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PlayerActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -88,6 +88,9 @@
<action
android:id="@+id/action_mediaInfoFragment_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" />
<action
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
app:destination="@id/playerActivity" />
</fragment>
<fragment
android:id="@+id/seasonFragment"
@ -123,4 +126,13 @@
android:name="episodeId"
app:argType="java.util.UUID" />
</dialog>
<activity
android:id="@+id/playerActivity"
android:name="dev.jdtech.jellyfin.PlayerActivity"
android:label="activity_player"
tools:layout="@layout/activity_player" >
<argument
android:name="itemId"
app:argType="java.util.UUID" />
</activity>
</navigation>