Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MPV #475

Closed
wants to merge 1 commit into from
Closed

MPV #475

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ android {
}
}

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

@Suppress("UnstableApiUsage")
buildFeatures {
viewBinding = true
Expand All @@ -89,6 +96,9 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
packagingOptions {
jniLibs.keepDebugSymbols += "**/*.so"
}
lint {
isAbortOnError = false
sarifReport = true
Expand Down Expand Up @@ -163,6 +173,11 @@ dependencies {
testImplementation(libs.bundles.kotest)
testImplementation(libs.mockk)
androidTestImplementation(libs.bundles.androidx.test)

// 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
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
159 changes: 159 additions & 0 deletions app/src/main/assets/skip-intro.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
MAX_SPEED = 100
ONE_SECOND = 1
skip = false
ov = mp.create_osd_overlay("ass-events")
ov.data = "Seeking..."
-- Max noise (dB) and min silence duration (s) to trigger
opts = { quietness = -30, duration = 0.5 }


function setOptions()
local options = require 'mp.options'
options.read_options(opts)
end

function setTime(time)
mp.set_property_number('time-pos', time)
end

function getTime()
return mp.get_property_native('time-pos')
end

function setSpeed(speed)
mp.set_property('speed', speed)
end

function getSpeed()
return mp.get_property('speed')
end

function setPause(state)
mp.set_property_bool('pause', state)
end

function setMute(state)
mp.set_property_bool('mute', state)
end

function initAudioFilter()
local af_table = mp.get_property_native('af')
af_table[#af_table + 1] = {
enabled = false,
label = 'silencedetect',
name = 'lavfi',
params = { graph = 'silencedetect=noise=' .. opts.quietness .. 'dB:d=' .. opts.duration }
}
mp.set_property_native('af', af_table)
end

function initVideoFilter()
local vf_table = mp.get_property_native('vf')
vf_table[#vf_table + 1] = {
enabled = false,
label = 'blackout',
name = 'lavfi',
params = { graph = '' }
}
mp.set_property_native('vf', vf_table)
end

function setAudioFilter(state)
local af_table = mp.get_property_native('af')
if #af_table > 0 then
for i = #af_table, 1, -1 do
if af_table[i].label == 'silencedetect' then
af_table[i].enabled = state
mp.set_property_native('af', af_table)
break
end
end
end
end

function dim(state)
local dim = { width = 0, height = 0 }
if state == true then
dim.width = mp.get_property_native('width')
dim.height = mp.get_property_native('height')
end
return dim.width .. 'x' .. dim.height
end

function setVideoFilter(state)
local vf_table = mp.get_property_native('vf')
if #vf_table > 0 then
for i = #vf_table, 1, -1 do
if vf_table[i].label == 'blackout' then
vf_table[i].enabled = state
vf_table[i].params = { graph = 'nullsink,color=c=black:s=' .. dim(state) }
mp.set_property_native('vf', vf_table)
break
end
end
end
end

function silenceTrigger(name, value)
if value == '{}' or value == nil then
return
end

local skipTime = tonumber(string.match(value, '%d+%.?%d+'))
local currTime = getTime()

if skipTime == nil or skipTime < currTime + ONE_SECOND then
return
end

stopSkip()
setTime(skipTime)
skip = false
end

function setAudioTrigger(state)
if state == true then
mp.observe_property('af-metadata/silencedetect', 'string', silenceTrigger)
else
mp.unobserve_property(silenceTrigger)
end
end

function startSkip()
ov:update()
startTime = getTime()
startSpeed = getSpeed()
-- This audio filter detects moments of silence
setAudioFilter(true)
-- This video filter makes fast-forward faster
setVideoFilter(true)
setAudioTrigger(true)
setPause(false)
setMute(true)
setSpeed(MAX_SPEED)
end

function stopSkip()
ov:remove()
setAudioFilter(false)
setVideoFilter(false)
setAudioTrigger(false)
setMute(false)
setSpeed(startSpeed)
end

function keypress()
skip = not skip
if skip then
startSkip()
else
stopSkip()
setTime(startTime)
end
end

setOptions(opts)
initAudioFilter()
initVideoFilter()

mp.add_key_binding(nil, 'skip-key', keypress)
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 @@ -17,12 +17,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 @@ -61,8 +63,9 @@ val applicationModule = module {
// Media player helpers
single { MediaSourceResolver(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 factories
single<DataSource.Factory> {
Expand Down
83 changes: 77 additions & 6 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 @@ -66,9 +67,9 @@ class DeviceProfileBuilder {
}

return DeviceProfile(
name = Constants.APP_INFO_NAME,
name = NativePlayer.PLAYER_NAME,
directPlayProfiles = directPlayProfiles,
transcodingProfiles = getTranscodingProfiles(),
transcodingProfiles = getExoPlayerTranscodingProfiles(),
containerProfiles = containerProfiles,
codecProfiles = codecProfiles,
subtitleProfiles = getSubtitleProfiles(EXO_EMBEDDED_SUBTITLES, EXO_EXTERNAL_SUBTITLES),
Expand All @@ -87,8 +88,32 @@ class DeviceProfileBuilder {
)
}

fun getMPVPlayerProfile(): DeviceProfile = DeviceProfile(
name = MPVPlayer.PLAYER_NAME,
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = getMPVTranscodingProfiles(),
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 @@ -145,7 +170,7 @@ class DeviceProfileBuilder {
return videoCodecs to audioCodecs
}

private fun getTranscodingProfiles(): List<TranscodingProfile> = ArrayList<TranscodingProfile>().apply {
private fun getExoPlayerTranscodingProfiles(): List<TranscodingProfile> = ArrayList<TranscodingProfile>().apply {
add(
TranscodingProfile(
type = DlnaProfileType.VIDEO,
Expand Down Expand Up @@ -207,6 +232,46 @@ class DeviceProfileBuilder {
)
}

private fun getMPVTranscodingProfiles(): List<TranscodingProfile> = ArrayList<TranscodingProfile>().apply {
add(
TranscodingProfile(
type = DlnaProfileType.AUDIO,
context = EncodingContext.STREAMING,

// TODO: remove redundant defaults after API/SDK is fixed
estimateContentLength = false,
enableMpegtsM2TsMode = false,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
copyTimestamps = false,
enableSubtitlesInManifest = false,
minSegments = 0,
segmentLength = 0,
breakOnNonKeyFrames = false,
)
)
add(
TranscodingProfile(
type = DlnaProfileType.VIDEO,
container = "ts",
videoCodec = "h264,h265,hevc,mpeg4,mpeg2video",
audioCodec = "mp1,mp2,mp3,aac,ac3,eac3,dts,mlp,truehd,opus,flac,vorbis",
context = EncodingContext.STREAMING,
protocol = "hls",
maxAudioChannels = "6",

// TODO: remove redundant defaults after API/SDK is fixed
estimateContentLength = false,
enableMpegtsM2TsMode = false,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
copyTimestamps = false,
enableSubtitlesInManifest = false,
minSegments = 0,
segmentLength = 0,
breakOnNonKeyFrames = false,
)
)
}

private fun getSubtitleProfiles(embedded: Array<String>, external: Array<String>): List<SubtitleProfile> = ArrayList<SubtitleProfile>().apply {
for (format in embedded) {
add(SubtitleProfile(format = format, method = SubtitleDeliveryMethod.EMBED))
Expand Down Expand Up @@ -304,5 +369,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"
)
}
}
Loading