diff --git a/app/build.gradle b/app/build.gradle
index b5c31eee..645bec73 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 610b4cf6..91e46363 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,13 +12,14 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Jellyfin">
+
+ android:theme="@style/Theme.JellyfinSplashScreen"
+ android:windowSoftInputMode="adjustPan">
diff --git a/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt
new file mode 100644
index 00000000..30a0140c
--- /dev/null
+++ b/app/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt
@@ -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")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt b/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
index 441b58bf..2284d04d 100644
--- a/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
+++ b/app/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
@@ -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
diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt
index dfad556e..eba68249 100644
--- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt
+++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt
@@ -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)
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt
index 84949023..ff2fcb74 100644
--- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt
+++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt
@@ -14,4 +14,6 @@ interface JellyfinRepository {
suspend fun getNextUp(seriesId: UUID): List
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID, fields: List? = null): List
+
+ suspend fun getStreamUrl(itemId: UUID): String
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt
index b751a0ef..94d3c8b6 100644
--- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt
+++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt
@@ -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
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt
new file mode 100644
index 00000000..41654374
--- /dev/null
+++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt
@@ -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()
+ var player: LiveData = _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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml
new file mode 100644
index 00000000..2df147d8
--- /dev/null
+++ b/app/src/main/res/layout/activity_player.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml
index 5972e47d..3ef322e7 100644
--- a/app/src/main/res/navigation/main_navigation.xml
+++ b/app/src/main/res/navigation/main_navigation.xml
@@ -88,6 +88,9 @@
+
+
+
+
\ No newline at end of file