Skip to content

Commit

Permalink
MPV
Browse files Browse the repository at this point in the history
  • Loading branch information
CarlosOlivo committed Jul 31, 2021
1 parent b7d6a64 commit 3259fb9
Show file tree
Hide file tree
Showing 19 changed files with 1,701 additions and 82 deletions.
15 changes: 15 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ android {
}
}

/*splits {
abi {
isEnable = true
isUniversalApk = true
}
}*/

@Suppress("UnstableApiUsage")
buildFeatures {
viewBinding = true
Expand All @@ -94,6 +101,9 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
packagingOptions {
doNotStrip("**/*.so")
}
lintOptions {
isAbortOnError = false
sarifReport = true
Expand Down Expand Up @@ -179,6 +189,11 @@ dependencies {

// Desugaring
coreLibraryDesugaring(Dependencies.Health.androidDesugarLibs)

// TODO: Decide how to build / publish this..
implementation(files("libmpv\\app-release.aar"))
//implementation(files("libmpv\\app-sources.jar"))
//implementation(files("libmpv\\app-javadoc.jar"))
}

tasks {
Expand Down
Binary file added app/libmpv/app-javadoc.jar
Binary file not shown.
Binary file added app/libmpv/app-release.aar
Binary file not shown.
Binary file added app/libmpv/app-sources.jar
Binary file not shown.
13 changes: 13 additions & 0 deletions app/src/main/assets/mpv.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# hwdec: try to use hardware decoding
hwdec=mediacodec-copy
hwdec-codecs="h264,hevc,mpeg4,mpeg2video,vp8,vp9"
gpu-dumb-mode=auto
# tls: allow self signed certificate
tls-verify=no
tls-ca-file=""
# demuxer: limit cache to 32 MiB, the default is too high for mobile devices
demuxer-max-bytes=32MiB
demuxer-max-back-bytes=32MiB
# sub: scale subtitles with video
sub-scale-with-window=no
sub-use-margins=no
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export class ExoPlayerPlugin {
export class NativePlayerPlugin {
constructor({ events, playbackManager, loading }) {
window['ExoPlayer'] = this;

this.events = events;
this.playbackManager = playbackManager;
this.loading = loading;

this.name = 'ExoPlayer';
this.name = 'NativePlayer';
this.type = 'mediaplayer';
this.id = 'exoplayer';
this.id = 'nativeplayer';

// Prioritize first
this.priority = -1;
Expand Down Expand Up @@ -164,7 +164,7 @@ export class ExoPlayerPlugin {

async getDeviceProfile() {
var profile = {};
profile.Name = 'ExoPlayer Stub';
profile.Name = 'NativePlayer Stub';
profile.MaxStreamingBitrate = 100000000;
profile.MaxStaticBitrate = 100000000;
profile.MusicStreamingTranscodingBitrate = 320000;
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/assets/native/nativeshell.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const features = [

const plugins = [
'NavigationPlugin',
'ExoPlayerPlugin',
'NativePlayerPlugin',
'ExternalPlayerPlugin'
];

Expand Down
7 changes: 5 additions & 2 deletions app/src/main/java/org/jellyfin/mobile/ApplicationModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import kotlinx.coroutines.channels.Channel
import okhttp3.OkHttpClient
import org.jellyfin.mobile.api.DeviceProfileBuilder
import org.jellyfin.mobile.bridge.ExternalPlayer
import org.jellyfin.mobile.bridge.NativePlayer
import org.jellyfin.mobile.controller.ApiController
import org.jellyfin.mobile.fragment.ConnectFragment
import org.jellyfin.mobile.fragment.WebViewFragment
import org.jellyfin.mobile.media.car.LibraryBrowser
import org.jellyfin.mobile.player.PlayerEvent
import org.jellyfin.mobile.player.PlayerFragment
import org.jellyfin.mobile.player.mpv.MPVPlayer
import org.jellyfin.mobile.player.source.MediaSourceResolver
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.PermissionRequestHelper
Expand Down Expand Up @@ -54,8 +56,9 @@ val applicationModule = module {
// Media player helpers
single { MediaSourceResolver(get(), get(), get()) }
single { DeviceProfileBuilder() }
single { get<DeviceProfileBuilder>().getDeviceProfile() }
single(named(ExternalPlayer.DEVICE_PROFILE_NAME)) { get<DeviceProfileBuilder>().getExternalPlayerProfile() }
single(named(ExternalPlayer.PLAYER_NAME)) { get<DeviceProfileBuilder>().getExternalPlayerProfile() }
single(named(MPVPlayer.PLAYER_NAME)) { get<DeviceProfileBuilder>().getMPVPlayerProfile() }
single(named(NativePlayer.PLAYER_NAME)) { get<DeviceProfileBuilder>().getExoPlayerProfile() }

// ExoPlayer data sources
single<DataSource.Factory> { DefaultDataSourceFactory(androidApplication(), Util.getUserAgent(androidApplication(), Constants.APP_INFO_NAME)) }
Expand Down
39 changes: 35 additions & 4 deletions app/src/main/java/org/jellyfin/mobile/api/DeviceProfileBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package org.jellyfin.mobile.api

import android.media.MediaCodecList
import org.jellyfin.mobile.bridge.ExternalPlayer
import org.jellyfin.mobile.bridge.NativePlayer
import org.jellyfin.mobile.player.DeviceCodec
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.player.mpv.MPVPlayer
import org.jellyfin.sdk.model.api.CodecProfile
import org.jellyfin.sdk.model.api.ContainerProfile
import org.jellyfin.sdk.model.api.DeviceProfile
Expand All @@ -21,7 +22,7 @@ class DeviceProfileBuilder {
require(SUPPORTED_CONTAINER_FORMATS.size == AVAILABLE_VIDEO_CODECS.size && SUPPORTED_CONTAINER_FORMATS.size == AVAILABLE_AUDIO_CODECS.size)
}

fun getDeviceProfile(): DeviceProfile {
fun getExoPlayerProfile(): DeviceProfile {
val containerProfiles = ArrayList<ContainerProfile>()
val directPlayProfiles = ArrayList<DirectPlayProfile>()
val codecProfiles = ArrayList<CodecProfile>()
Expand Down Expand Up @@ -62,7 +63,7 @@ class DeviceProfileBuilder {
}

return DeviceProfile(
name = Constants.APP_INFO_NAME,
name = NativePlayer.PLAYER_NAME,
directPlayProfiles = directPlayProfiles,
transcodingProfiles = getTranscodingProfiles(),
containerProfiles = containerProfiles,
Expand All @@ -83,8 +84,32 @@ class DeviceProfileBuilder {
)
}

fun getMPVPlayerProfile(): DeviceProfile = DeviceProfile(
name = MPVPlayer.PLAYER_NAME,
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = getTranscodingProfiles(),
containerProfiles = emptyList(),
codecProfiles = emptyList(),
subtitleProfiles = getSubtitleProfiles(MPV_PLAYER_SUBTITLES.plus("vtt"), MPV_PLAYER_SUBTITLES),

// TODO: remove redundant defaults after API/SDK is fixed
maxAlbumArtWidth = Int.MAX_VALUE,
maxAlbumArtHeight = Int.MAX_VALUE,
timelineOffsetSeconds = 0,
enableAlbumArtInDidl = false,
enableSingleAlbumArtLimit = false,
enableSingleSubtitleLimit = false,
requiresPlainFolders = false,
requiresPlainVideoItems = false,
enableMsMediaReceiverRegistrar = false,
ignoreTranscodeByteRangeRequests = false,
)

fun getExternalPlayerProfile(): DeviceProfile = DeviceProfile(
name = ExternalPlayer.DEVICE_PROFILE_NAME,
name = ExternalPlayer.PLAYER_NAME,
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
Expand Down Expand Up @@ -294,5 +319,11 @@ class DeviceProfileBuilder {
private val EXTERNAL_PLAYER_SUBTITLES = arrayOf(
"ssa", "ass", "srt", "subrip", "idx", "sub", "vtt", "webvtt", "ttml", "pgs", "pgssub", "smi", "smil"
)
// https://github.com/mpv-player/mpv/blob/6857600c47f069aeb68232a745bc8f81d45c9967/player/external_files.c#L35
private val MPV_PLAYER_SUBTITLES = arrayOf(
"idx", "sub", "srt", "rt", "ssa", "ass", "mks",/* "vtt", */"sup", "scc", "smi", "lrc", "pgs",
// https://ffmpeg.org/general.html#Subtitle-Formats
"aqt", "jss", "txt", "mpsub", "pjs", "sami", "stl"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ExternalPlayer(
private val appPreferences: AppPreferences by inject()
private val webappFunctionChannel: WebappFunctionChannel by inject()
private val mediaSourceResolver: MediaSourceResolver by inject()
private val externalPlayerProfile: DeviceProfile by inject(named(DEVICE_PROFILE_NAME))
private val externalPlayerProfile: DeviceProfile by inject(named(PLAYER_NAME))
private val videosApi: VideosApi by inject()
private val apiClient: ApiClient by inject()

Expand Down Expand Up @@ -293,6 +293,6 @@ class ExternalPlayer(
}

companion object {
const val DEVICE_PROFILE_NAME = "Android External Player"
const val PLAYER_NAME = "Android External Player"
}
}
6 changes: 5 additions & 1 deletion app/src/main/java/org/jellyfin/mobile/bridge/NativePlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class NativePlayer(private val host: NativePlayerHost) : KoinComponent {
private val playerEventChannel: Channel<PlayerEvent> by inject(named(PLAYER_EVENT_CHANNEL))

@JavascriptInterface
fun isEnabled() = appPreferences.videoPlayerType == VideoPlayerType.EXO_PLAYER
fun isEnabled() = appPreferences.videoPlayerType in arrayOf(VideoPlayerType.EXO_PLAYER, VideoPlayerType.MPV_PLAYER)

@JavascriptInterface
fun loadPlayer(args: String) {
Expand Down Expand Up @@ -61,4 +61,8 @@ class NativePlayer(private val host: NativePlayerHost) : KoinComponent {
fun setVolume(volume: Int) {
playerEventChannel.trySend(PlayerEvent.SetVolume(volume))
}

companion object {
const val PLAYER_NAME = Constants.APP_INFO_NAME
}
}
71 changes: 48 additions & 23 deletions app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jellyfin.mobile.AppPreferences
import org.jellyfin.mobile.BuildConfig
import org.jellyfin.mobile.PLAYER_EVENT_CHANNEL
import org.jellyfin.mobile.bridge.ExternalPlayer
import org.jellyfin.mobile.bridge.NativePlayer
import org.jellyfin.mobile.player.mpv.MPVPlayer
import org.jellyfin.mobile.player.source.JellyfinMediaSource
import org.jellyfin.mobile.player.source.MediaQueueManager
import org.jellyfin.mobile.settings.VideoPlayerType
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.Constants.SUPPORTED_VIDEO_PLAYER_PLAYBACK_ACTIONS
import org.jellyfin.mobile.utils.applyDefaultAudioAttributes
import org.jellyfin.mobile.utils.applyDefaultLocalAudioAttributes
import org.jellyfin.mobile.utils.getRendererIndexByType
import org.jellyfin.mobile.utils.getVolumeLevelPercent
import org.jellyfin.mobile.utils.getVolumeRange
import org.jellyfin.mobile.utils.scaleInRange
Expand All @@ -52,29 +56,36 @@ import org.koin.core.qualifier.named
import timber.log.Timber

class PlayerViewModel(application: Application) : AndroidViewModel(application), KoinComponent, Player.Listener {
private val appPreferences by inject<AppPreferences>()
private val playStateApi by inject<PlayStateApi>()

private val lifecycleObserver = PlayerLifecycleObserver(this)
private val audioManager: AudioManager by lazy { getApplication<Application>().getSystemService()!! }
val notificationHelper: PlayerNotificationHelper by lazy { PlayerNotificationHelper(this) }

// Media source handling
val mediaQueueManager = MediaQueueManager(this)
val mediaQueueManager = MediaQueueManager(
viewModel = this, playerName = when (appPreferences.videoPlayerType) {
VideoPlayerType.EXO_PLAYER -> NativePlayer.PLAYER_NAME
VideoPlayerType.MPV_PLAYER -> MPVPlayer.PLAYER_NAME
else -> ExternalPlayer.PLAYER_NAME
}
)
val mediaSourceOrNull: JellyfinMediaSource?
get() = mediaQueueManager.mediaQueue.value?.jellyfinMediaSource

// ExoPlayer
private val _player = MutableLiveData<ExoPlayer?>()
// Player
private val _player = MutableLiveData<Player?>()
private val _playerState = MutableLiveData<Int>()
val player: LiveData<ExoPlayer?> get() = _player
val player: LiveData<Player?> get() = _player
val playerState: LiveData<Int> get() = _playerState

private var progressUpdateJob: Job? = null

/**
* Returns the current ExoPlayer instance or null
* Returns the current Player instance or null
*/
val playerOrNull: ExoPlayer? get() = _player.value
val playerOrNull: Player? get() = _player.value

private val playerEventChannel: Channel<PlayerEvent> by inject(named(PLAYER_EVENT_CHANNEL))

Expand Down Expand Up @@ -109,24 +120,37 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),
}

/**
* Setup a new [SimpleExoPlayer] for video playback, register callbacks and set attributes
* Setup a new [Player] for video playback, register callbacks and set attributes
*/
fun setupPlayer() {
_player.value = SimpleExoPlayer.Builder(getApplication()).apply {
setTrackSelector(mediaQueueManager.trackSelector)
if (BuildConfig.DEBUG) {
setAnalyticsCollector(AnalyticsCollector(Clock.DEFAULT).apply {
addListener(mediaQueueManager.eventLogger)
})
_player.value = when (appPreferences.videoPlayerType) {
VideoPlayerType.EXO_PLAYER -> {
SimpleExoPlayer.Builder(getApplication()).apply {
setTrackSelector(mediaQueueManager.trackSelector)
if (BuildConfig.DEBUG) {
setAnalyticsCollector(AnalyticsCollector(Clock.DEFAULT).apply {
addListener(mediaQueueManager.eventLogger)
})
}
}.build().apply {
addListener(this@PlayerViewModel)
applyDefaultAudioAttributes(C.CONTENT_TYPE_MOVIE)
}
}
VideoPlayerType.MPV_PLAYER -> {
MPVPlayer(
context = getApplication(),
requestAudioFocus = true
).apply {
addListener(this@PlayerViewModel)
}
}
}.build().apply {
addListener(this@PlayerViewModel)
applyDefaultAudioAttributes(C.CONTENT_TYPE_MOVIE)
else -> null
}
}

/**
* Release the current ExoPlayer and stop/release the current MediaSession
* Release the current [Player] and stop/release the current [MediaSession]
*/
private fun releasePlayer() {
notificationHelper.dismissNotification()
Expand All @@ -141,7 +165,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),

fun play(queueItem: MediaQueueManager.QueueItem.Loaded) {
val player = playerOrNull ?: return
player.setMediaSource(queueItem.exoMediaSource)
if (player is ExoPlayer) {
player.setMediaSource(queueItem.exoMediaSource)
}
if (player is MPVPlayer) {
player.setMediaItem(queueItem.exoMediaSource.mediaItem)
}
player.prepare()
val startTime = queueItem.jellyfinMediaSource.startTimeMs
if (startTime > 0) {
Expand Down Expand Up @@ -325,10 +354,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application),
audioManager.setStreamVolume(stream, scaled, 0)
}

fun getPlayerRendererIndex(type: Int): Int {
return playerOrNull?.getRendererIndexByType(type) ?: -1
}

@SuppressLint("SwitchIntDef")
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
val player = playerOrNull ?: return
Expand Down
Loading

0 comments on commit 3259fb9

Please sign in to comment.