Get mediaSources and select version of movie
This commit is contained in:
parent
7f5dea58bc
commit
e69a653453
11 changed files with 133 additions and 61 deletions
|
@ -12,7 +12,7 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Jellyfin">
|
android:theme="@style/Theme.Jellyfin">
|
||||||
<activity android:name=".PlayerActivity"></activity>
|
<activity android:name=".PlayerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main" />
|
android:label="@string/title_activity_main" />
|
||||||
|
|
|
@ -34,7 +34,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (viewModel.player.value == null) {
|
if (viewModel.player.value == null) {
|
||||||
viewModel.initializePlayer(args.itemId)
|
viewModel.initializePlayer(args.itemId, args.mediaSourceId)
|
||||||
}
|
}
|
||||||
hideSystemUI()
|
hideSystemUI()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package dev.jdtech.jellyfin.dialogs
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
|
class VideoVersionDialogFragment(
|
||||||
|
private val viewModel: MediaInfoViewModel
|
||||||
|
) : DialogFragment() {
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val items = viewModel.mediaSources.value!!.map { it.name }
|
||||||
|
return activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setTitle("Select a version")
|
||||||
|
.setItems(items.toTypedArray()) { _, which ->
|
||||||
|
viewModel.navigateToPlayer(viewModel.mediaSources.value!![which])
|
||||||
|
}
|
||||||
|
builder.create()
|
||||||
|
} ?: throw IllegalStateException("Activity cannot be null")
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,7 +81,8 @@ class HomeFragment : Fragment() {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
||||||
item.id,
|
item.id,
|
||||||
item.name
|
item.name,
|
||||||
|
item.type ?: "Unknown"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,8 @@ class LibraryFragment : Fragment() {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment(
|
LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment(
|
||||||
item.id,
|
item.id,
|
||||||
item.name
|
item.name,
|
||||||
|
item.type ?: "Unknown"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
||||||
|
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -53,6 +54,10 @@ class MediaInfoFragment : Fragment() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
viewModel.navigateToPlayer.observe(viewLifecycleOwner, { mediaSource ->
|
||||||
|
mediaSource.id?.let { navigateToPlayerActivity(args.itemId, it) }
|
||||||
|
})
|
||||||
|
|
||||||
binding.trailerButton.setOnClickListener {
|
binding.trailerButton.setOnClickListener {
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
|
@ -72,10 +77,24 @@ class MediaInfoFragment : Fragment() {
|
||||||
binding.peopleRecyclerView.adapter = PersonListAdapter()
|
binding.peopleRecyclerView.adapter = PersonListAdapter()
|
||||||
|
|
||||||
binding.playButton.setOnClickListener {
|
binding.playButton.setOnClickListener {
|
||||||
navigateToPlayerActivity(args.itemId)
|
if (args.itemType == "Movie") {
|
||||||
|
if (!viewModel.mediaSources.value.isNullOrEmpty()) {
|
||||||
|
if (viewModel.mediaSources.value!!.size > 1) {
|
||||||
|
VideoVersionDialogFragment(viewModel).show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"videoversiondialog"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
navigateToPlayerActivity(
|
||||||
|
args.itemId,
|
||||||
|
viewModel.mediaSources.value!![0].id!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadData(args.itemId)
|
viewModel.loadData(args.itemId, args.itemType)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
|
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
|
||||||
|
@ -97,9 +116,12 @@ class MediaInfoFragment : Fragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToPlayerActivity(itemId: UUID) {
|
private fun navigateToPlayerActivity(itemId: UUID, mediaSourceId: String) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(itemId)
|
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
|
||||||
|
itemId,
|
||||||
|
mediaSourceId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.repository
|
||||||
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface JellyfinRepository {
|
interface JellyfinRepository {
|
||||||
|
@ -15,5 +16,7 @@ interface JellyfinRepository {
|
||||||
|
|
||||||
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID, fields: List<ItemFields>? = null): List<BaseItemDto>
|
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID, fields: List<ItemFields>? = null): List<BaseItemDto>
|
||||||
|
|
||||||
suspend fun getStreamUrl(itemId: UUID): String
|
suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo>
|
||||||
|
|
||||||
|
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String
|
||||||
}
|
}
|
|
@ -61,52 +61,57 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
return episodes
|
return episodes
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStreamUrl(itemId: UUID): String {
|
override suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo> {
|
||||||
val streamUrl: String
|
val mediaSourceInfoList: List<MediaSourceInfo>
|
||||||
|
val mediaInfo by 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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mediaSourceInfoList = mediaInfo.mediaSources ?: listOf()
|
||||||
|
return mediaSourceInfoList
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String {
|
||||||
|
var streamUrl: String = ""
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val mediaInfo by jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
streamUrl = jellyfinApi.videosApi.getVideoStreamUrl(
|
||||||
itemId, PlaybackInfoDto(
|
itemId,
|
||||||
userId = jellyfinApi.userId!!,
|
static = true,
|
||||||
deviceProfile = DeviceProfile(
|
mediaSourceId = mediaSourceId
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
Log.d("JellyfinRepository", mediaInfo.mediaSources.toString())
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("JellyfinRepository", "${e.message}")
|
Log.e("JellyfinRepository", "${e.message}")
|
||||||
}
|
}
|
||||||
streamUrl = jellyfinApi.videosApi.getVideoStreamUrl(
|
|
||||||
itemId,
|
|
||||||
static = true,
|
|
||||||
mediaSourceId = itemId.toString()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return streamUrl
|
return streamUrl
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -49,7 +50,13 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
private val _seasons = MutableLiveData<List<BaseItemDto>>()
|
private val _seasons = MutableLiveData<List<BaseItemDto>>()
|
||||||
val seasons: LiveData<List<BaseItemDto>> = _seasons
|
val seasons: LiveData<List<BaseItemDto>> = _seasons
|
||||||
|
|
||||||
fun loadData(itemId: UUID) {
|
private val _mediaSources = MutableLiveData<List<MediaSourceInfo>>()
|
||||||
|
val mediaSources: LiveData<List<MediaSourceInfo>> = _mediaSources
|
||||||
|
|
||||||
|
private val _navigateToPlayer = MutableLiveData<MediaSourceInfo>()
|
||||||
|
val navigateToPlayer: LiveData<MediaSourceInfo> = _navigateToPlayer
|
||||||
|
|
||||||
|
fun loadData(itemId: UUID, itemType: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_item.value = jellyfinRepository.getItem(itemId)
|
_item.value = jellyfinRepository.getItem(itemId)
|
||||||
_actors.value = getActors(_item.value!!)
|
_actors.value = getActors(_item.value!!)
|
||||||
|
@ -60,10 +67,13 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
_genresString.value = _item.value?.genres?.joinToString(separator = ", ")
|
_genresString.value = _item.value?.genres?.joinToString(separator = ", ")
|
||||||
_runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min"
|
_runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min"
|
||||||
_dateString.value = getDateString(_item.value!!)
|
_dateString.value = getDateString(_item.value!!)
|
||||||
if (_item.value!!.type == "Series") {
|
if (itemType == "Series") {
|
||||||
_nextUp.value = getNextUp(itemId)
|
_nextUp.value = getNextUp(itemId)
|
||||||
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
||||||
}
|
}
|
||||||
|
if (itemType == "Movie") {
|
||||||
|
_mediaSources.value = jellyfinRepository.getMediaSources(itemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,4 +130,8 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
else -> dateString
|
else -> dateString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun navigateToPlayer(mediaSource: MediaSourceInfo) {
|
||||||
|
_navigateToPlayer.value = mediaSource
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import android.media.MediaCodecInfo
|
|
||||||
import android.media.MediaCodecList
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -11,7 +8,6 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -37,7 +33,7 @@ constructor(
|
||||||
playbackStateListener = PlaybackStateListener()
|
playbackStateListener = PlaybackStateListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initializePlayer(itemId: UUID) {
|
fun initializePlayer(itemId: UUID, mediaSourceId: String) {
|
||||||
if (player.value == null) {
|
if (player.value == null) {
|
||||||
val trackSelector = DefaultTrackSelector(application)
|
val trackSelector = DefaultTrackSelector(application)
|
||||||
val renderersFactory = DefaultRenderersFactory(application).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
val renderersFactory = DefaultRenderersFactory(application).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||||
|
@ -50,7 +46,7 @@ constructor(
|
||||||
player.value?.addListener(playbackStateListener)
|
player.value?.addListener(playbackStateListener)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val streamUrl = jellyfinRepository.getStreamUrl(itemId)
|
val streamUrl = jellyfinRepository.getStreamUrl(itemId, mediaSourceId)
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
MediaItem.Builder()
|
MediaItem.Builder()
|
||||||
.setMediaId(itemId.toString())
|
.setMediaId(itemId.toString())
|
||||||
|
@ -80,16 +76,16 @@ constructor(
|
||||||
var stateString = "UNKNOWN_STATE -"
|
var stateString = "UNKNOWN_STATE -"
|
||||||
when (state) {
|
when (state) {
|
||||||
ExoPlayer.STATE_IDLE -> {
|
ExoPlayer.STATE_IDLE -> {
|
||||||
stateString = "ExoPlayer.STATE_IDLE -";
|
stateString = "ExoPlayer.STATE_IDLE -"
|
||||||
}
|
}
|
||||||
ExoPlayer.STATE_BUFFERING -> {
|
ExoPlayer.STATE_BUFFERING -> {
|
||||||
stateString = "ExoPlayer.STATE_BUFFERING -";
|
stateString = "ExoPlayer.STATE_BUFFERING -"
|
||||||
}
|
}
|
||||||
ExoPlayer.STATE_READY -> {
|
ExoPlayer.STATE_READY -> {
|
||||||
stateString = "ExoPlayer.STATE_READY -";
|
stateString = "ExoPlayer.STATE_READY -"
|
||||||
}
|
}
|
||||||
ExoPlayer.STATE_ENDED -> {
|
ExoPlayer.STATE_ENDED -> {
|
||||||
stateString = "ExoPlayer.STATE_ENDED -";
|
stateString = "ExoPlayer.STATE_ENDED -"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d("PlayerActivity", "changed state to $stateString")
|
Log.d("PlayerActivity", "changed state to $stateString")
|
||||||
|
|
|
@ -91,6 +91,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
||||||
app:destination="@id/playerActivity" />
|
app:destination="@id/playerActivity" />
|
||||||
|
<argument
|
||||||
|
android:name="itemType"
|
||||||
|
app:argType="string" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/seasonFragment"
|
android:id="@+id/seasonFragment"
|
||||||
|
@ -134,5 +137,8 @@
|
||||||
<argument
|
<argument
|
||||||
android:name="itemId"
|
android:name="itemId"
|
||||||
app:argType="java.util.UUID" />
|
app:argType="java.util.UUID" />
|
||||||
|
<argument
|
||||||
|
android:name="mediaSourceId"
|
||||||
|
app:argType="string" />
|
||||||
</activity>
|
</activity>
|
||||||
</navigation>
|
</navigation>
|
Loading…
Reference in a new issue