First early test of ExoPlayer
Currently only plays some movies
This commit is contained in:
parent
4bbf40bc22
commit
fcbd7d1f33
10 changed files with 248 additions and 4 deletions
|
@ -87,6 +87,12 @@ dependencies {
|
||||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||||
kapt "com.google.dagger:hilt-compiler:$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
|
// Testing
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
|
|
|
@ -12,13 +12,14 @@
|
||||||
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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main" />
|
android:label="@string/title_activity_main" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".SetupActivity"
|
android:name=".SetupActivity"
|
||||||
android:windowSoftInputMode="adjustPan"
|
android:theme="@style/Theme.JellyfinSplashScreen"
|
||||||
android:theme="@style/Theme.JellyfinSplashScreen">
|
android:windowSoftInputMode="adjustPan">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|
47
app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt
Normal file
47
app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ class JellyfinApi(context: Context, baseUrl: String) {
|
||||||
val userLibraryApi = UserLibraryApi(api)
|
val userLibraryApi = UserLibraryApi(api)
|
||||||
val showsApi = TvShowsApi(api)
|
val showsApi = TvShowsApi(api)
|
||||||
val sessionApi = SessionApi(api)
|
val sessionApi = SessionApi(api)
|
||||||
|
val videosApi = VideosApi(api)
|
||||||
|
val mediaInfoApi = MediaInfoApi(api)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|
|
@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
||||||
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.*
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MediaInfoFragment : Fragment() {
|
class MediaInfoFragment : Fragment() {
|
||||||
|
@ -70,6 +71,10 @@ class MediaInfoFragment : Fragment() {
|
||||||
}, fixedWidth = true)
|
}, fixedWidth = true)
|
||||||
binding.peopleRecyclerView.adapter = PersonListAdapter()
|
binding.peopleRecyclerView.adapter = PersonListAdapter()
|
||||||
|
|
||||||
|
binding.playButton.setOnClickListener {
|
||||||
|
navigateToPlayerActivity(args.itemId)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.loadData(args.itemId)
|
viewModel.loadData(args.itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,4 +96,10 @@ class MediaInfoFragment : Fragment() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateToPlayerActivity(itemId: UUID) {
|
||||||
|
findNavController().navigate(
|
||||||
|
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(itemId)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,4 +14,6 @@ interface JellyfinRepository {
|
||||||
suspend fun getNextUp(seriesId: UUID): List<BaseItemDto>
|
suspend fun getNextUp(seriesId: UUID): List<BaseItemDto>
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
|
@ -3,8 +3,7 @@ package dev.jdtech.jellyfin.repository
|
||||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.*
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRepository {
|
class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRepository {
|
||||||
|
@ -60,4 +59,49 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
}
|
}
|
||||||
return episodes
|
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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
13
app/src/main/res/layout/activity_player.xml
Normal file
13
app/src/main/res/layout/activity_player.xml
Normal 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>
|
|
@ -88,6 +88,9 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_mediaInfoFragment_to_episodeBottomSheetFragment"
|
android:id="@+id/action_mediaInfoFragment_to_episodeBottomSheetFragment"
|
||||||
app:destination="@id/episodeBottomSheetFragment" />
|
app:destination="@id/episodeBottomSheetFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
||||||
|
app:destination="@id/playerActivity" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/seasonFragment"
|
android:id="@+id/seasonFragment"
|
||||||
|
@ -123,4 +126,13 @@
|
||||||
android:name="episodeId"
|
android:name="episodeId"
|
||||||
app:argType="java.util.UUID" />
|
app:argType="java.util.UUID" />
|
||||||
</dialog>
|
</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>
|
</navigation>
|
Loading…
Reference in a new issue