mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
WIP
This commit is contained in:
parent
77c787b51f
commit
1ee834d42b
37 changed files with 2322 additions and 18 deletions
|
|
@ -134,15 +134,25 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val outputFileName =
|
||||
"Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}.apk"
|
||||
output.outputFileName = outputFileName
|
||||
}
|
||||
// applicationVariants.all {
|
||||
// val variant = this
|
||||
// variant.outputs
|
||||
// .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
// .forEach { output ->
|
||||
// val outputFileName =
|
||||
// "Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}.apk"
|
||||
// output.outputFileName = outputFileName
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a")
|
||||
// include("armeabi-v7a") // TODO
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,6 +185,25 @@ aboutLibraries {
|
|||
duplicationRule = DuplicateRule.SIMPLE
|
||||
}
|
||||
}
|
||||
|
||||
// Map versionCode so each ABI gets a different one
|
||||
// e.g. x86 with version 21 gets a versionCode of 3021
|
||||
// val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86" to 3, "x86_64" to 4)
|
||||
// val universalBase = 8000
|
||||
// android.applicationVariants.all { variant ->
|
||||
// variant.outputs.forEach { output ->
|
||||
// val base = output.filters.firstOrNull { it.filterType==VariantOutput.ABI }
|
||||
// // val base = abiCodes.get(output.getFilter(VariantOutput.ABI))
|
||||
// // universal APK just gets a constant added to it
|
||||
// if (base != null) {
|
||||
// output.versionCodeOverride = base * 1000 + variant.versionCode
|
||||
// } else {
|
||||
// output.versionCodeOverride = universalBase + variant.versionCode
|
||||
// }
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ fun PlaybackPage(
|
|||
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
|
||||
modifier = scaledModifier,
|
||||
)
|
||||
if (presentationState.coverSurface) {
|
||||
if (false && presentationState.coverSurface) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import androidx.media3.common.TrackSelectionOverride
|
|||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
|
||||
|
|
@ -53,6 +52,7 @@ import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
|
|||
import com.github.damontecres.wholphin.util.TrackSupport
|
||||
import com.github.damontecres.wholphin.util.checkForSupport
|
||||
import com.github.damontecres.wholphin.util.formatDateTime
|
||||
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||
import com.github.damontecres.wholphin.util.seasonEpisodePadded
|
||||
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||
|
|
@ -131,13 +131,14 @@ class PlaybackViewModel
|
|||
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||
}
|
||||
ExoPlayer
|
||||
.Builder(context)
|
||||
.setRenderersFactory(
|
||||
DefaultRenderersFactory(context)
|
||||
.setEnableDecoderFallback(true)
|
||||
.setExtensionRendererMode(rendererMode),
|
||||
).build()
|
||||
// ExoPlayer
|
||||
// .Builder(context)
|
||||
// .setRenderersFactory(
|
||||
// DefaultRenderersFactory(context)
|
||||
// .setEnableDecoderFallback(true)
|
||||
// .setExtensionRendererMode(rendererMode),
|
||||
// ).build()
|
||||
MpvPlayer(context)
|
||||
.apply {
|
||||
playWhenReady = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.view.Surface
|
||||
|
||||
// Wrapper for native library
|
||||
|
||||
@Suppress("unused")
|
||||
object MPVLib {
|
||||
init {
|
||||
val libs = arrayOf("mpv", "player")
|
||||
for (lib in libs) {
|
||||
System.loadLibrary(lib)
|
||||
}
|
||||
}
|
||||
|
||||
external fun create(appctx: Context)
|
||||
|
||||
external fun init()
|
||||
|
||||
external fun destroy()
|
||||
|
||||
external fun attachSurface(surface: Surface)
|
||||
|
||||
external fun detachSurface()
|
||||
|
||||
external fun command(cmd: Array<out String>)
|
||||
|
||||
external fun setOptionString(
|
||||
name: String,
|
||||
value: String,
|
||||
): Int
|
||||
|
||||
external fun grabThumbnail(dimension: Int): Bitmap?
|
||||
|
||||
external fun getPropertyInt(property: String): Int?
|
||||
|
||||
external fun setPropertyInt(
|
||||
property: String,
|
||||
value: Int,
|
||||
)
|
||||
|
||||
external fun getPropertyDouble(property: String): Double?
|
||||
|
||||
external fun setPropertyDouble(
|
||||
property: String,
|
||||
value: Double,
|
||||
)
|
||||
|
||||
external fun getPropertyBoolean(property: String): Boolean?
|
||||
|
||||
external fun setPropertyBoolean(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
)
|
||||
|
||||
external fun getPropertyString(property: String): String?
|
||||
|
||||
external fun setPropertyString(
|
||||
property: String,
|
||||
value: String,
|
||||
)
|
||||
|
||||
external fun observeProperty(
|
||||
property: String,
|
||||
format: Int,
|
||||
)
|
||||
|
||||
private val observers = mutableListOf<EventObserver>()
|
||||
|
||||
@JvmStatic
|
||||
fun addObserver(o: EventObserver) {
|
||||
synchronized(observers) {
|
||||
observers.add(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeObserver(o: EventObserver) {
|
||||
synchronized(observers) {
|
||||
observers.remove(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Long,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Double,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: String,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(property: String) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun event(eventId: Int) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.event(eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val log_observers = mutableListOf<LogObserver>()
|
||||
|
||||
@JvmStatic
|
||||
fun addLogObserver(o: LogObserver) {
|
||||
synchronized(log_observers) {
|
||||
log_observers.add(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeLogObserver(o: LogObserver) {
|
||||
synchronized(log_observers) {
|
||||
log_observers.remove(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logMessage(
|
||||
prefix: String,
|
||||
level: Int,
|
||||
text: String,
|
||||
) {
|
||||
synchronized(log_observers) {
|
||||
for (o in log_observers) {
|
||||
o.logMessage(prefix, level, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventObserver {
|
||||
fun eventProperty(property: String)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Long,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: String,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Double,
|
||||
)
|
||||
|
||||
fun event(eventId: Int)
|
||||
}
|
||||
|
||||
interface LogObserver {
|
||||
fun logMessage(
|
||||
prefix: String,
|
||||
level: Int,
|
||||
text: String,
|
||||
)
|
||||
}
|
||||
|
||||
object MpvFormat {
|
||||
const val MPV_FORMAT_NONE: Int = 0
|
||||
const val MPV_FORMAT_STRING: Int = 1
|
||||
const val MPV_FORMAT_OSD_STRING: Int = 2
|
||||
const val MPV_FORMAT_FLAG: Int = 3
|
||||
const val MPV_FORMAT_INT64: Int = 4
|
||||
const val MPV_FORMAT_DOUBLE: Int = 5
|
||||
const val MPV_FORMAT_NODE: Int = 6
|
||||
const val MPV_FORMAT_NODE_ARRAY: Int = 7
|
||||
const val MPV_FORMAT_NODE_MAP: Int = 8
|
||||
const val MPV_FORMAT_BYTE_ARRAY: Int = 9
|
||||
}
|
||||
|
||||
object MpvEvent {
|
||||
const val MPV_EVENT_NONE: Int = 0
|
||||
const val MPV_EVENT_SHUTDOWN: Int = 1
|
||||
const val MPV_EVENT_LOG_MESSAGE: Int = 2
|
||||
const val MPV_EVENT_GET_PROPERTY_REPLY: Int = 3
|
||||
const val MPV_EVENT_SET_PROPERTY_REPLY: Int = 4
|
||||
const val MPV_EVENT_COMMAND_REPLY: Int = 5
|
||||
const val MPV_EVENT_START_FILE: Int = 6
|
||||
const val MPV_EVENT_END_FILE: Int = 7
|
||||
const val MPV_EVENT_FILE_LOADED: Int = 8
|
||||
|
||||
@Deprecated("")
|
||||
const val MPV_EVENT_IDLE: Int = 11
|
||||
|
||||
@Deprecated("")
|
||||
const val MPV_EVENT_TICK: Int = 14
|
||||
const val MPV_EVENT_CLIENT_MESSAGE: Int = 16
|
||||
const val MPV_EVENT_VIDEO_RECONFIG: Int = 17
|
||||
const val MPV_EVENT_AUDIO_RECONFIG: Int = 18
|
||||
const val MPV_EVENT_SEEK: Int = 20
|
||||
const val MPV_EVENT_PLAYBACK_RESTART: Int = 21
|
||||
const val MPV_EVENT_PROPERTY_CHANGE: Int = 22
|
||||
const val MPV_EVENT_QUEUE_OVERFLOW: Int = 24
|
||||
const val MPV_EVENT_HOOK: Int = 25
|
||||
}
|
||||
|
||||
object MpvLogLevel {
|
||||
const val MPV_LOG_LEVEL_NONE: Int = 0
|
||||
const val MPV_LOG_LEVEL_FATAL: Int = 10
|
||||
const val MPV_LOG_LEVEL_ERROR: Int = 20
|
||||
const val MPV_LOG_LEVEL_WARN: Int = 30
|
||||
const val MPV_LOG_LEVEL_INFO: Int = 40
|
||||
const val MPV_LOG_LEVEL_V: Int = 50
|
||||
const val MPV_LOG_LEVEL_DEBUG: Int = 60
|
||||
const val MPV_LOG_LEVEL_TRACE: Int = 70
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.BasePlayer
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class MpvPlayer(
|
||||
private val context: Context,
|
||||
) : BasePlayer() {
|
||||
private var surface: Surface? = null
|
||||
private val listeners = mutableListOf<Player.Listener>()
|
||||
private val looper = Util.getCurrentOrMainLooper()
|
||||
private val availableCommands: Player.Commands
|
||||
private lateinit var surfaceSize: Size
|
||||
|
||||
private var mediaItem: MediaItem? = null
|
||||
|
||||
init {
|
||||
MPVLib.create(context)
|
||||
MPVLib.init()
|
||||
MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy")
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
MPVLib.setOptionString("opengl-es", "yes")
|
||||
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
||||
val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32
|
||||
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
|
||||
MPVLib.setOptionString("force-window", "no")
|
||||
// need to idle at least once for playFile() logic to work
|
||||
MPVLib.setOptionString("idle", "once")
|
||||
|
||||
availableCommands =
|
||||
Player.Commands
|
||||
.Builder()
|
||||
.addAll(
|
||||
COMMAND_PLAY_PAUSE,
|
||||
COMMAND_PREPARE,
|
||||
COMMAND_STOP,
|
||||
COMMAND_SET_SPEED_AND_PITCH,
|
||||
COMMAND_SET_SHUFFLE_MODE,
|
||||
COMMAND_SET_REPEAT_MODE,
|
||||
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
COMMAND_GET_TIMELINE,
|
||||
COMMAND_GET_METADATA,
|
||||
// COMMAND_SET_PLAYLIST_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
// COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_TRACKS,
|
||||
// COMMAND_GET_AUDIO_ATTRIBUTES,
|
||||
// COMMAND_SET_AUDIO_ATTRIBUTES,
|
||||
// COMMAND_GET_VOLUME,
|
||||
// COMMAND_SET_VOLUME,
|
||||
COMMAND_SET_VIDEO_SURFACE,
|
||||
// COMMAND_GET_TEXT,
|
||||
COMMAND_RELEASE,
|
||||
).build()
|
||||
}
|
||||
|
||||
override fun getApplicationLooper(): Looper = looper
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
resetPosition: Boolean,
|
||||
) {
|
||||
mediaItems.firstOrNull()?.let {
|
||||
mediaItem = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
startPositionMs: Long,
|
||||
) {
|
||||
setMediaItems(mediaItems.subList(startIndex, mediaItems.size), false)
|
||||
seekTo(startPositionMs)
|
||||
}
|
||||
|
||||
override fun addMediaItems(
|
||||
index: Int,
|
||||
mediaItems: List<MediaItem>,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun moveMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
newIndex: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun replaceMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
mediaItems: List<MediaItem>,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun removeMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
|
||||
override fun prepare() {
|
||||
}
|
||||
|
||||
override fun getPlaybackState(): Int {
|
||||
val state = STATE_READY
|
||||
return state
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
|
||||
override fun getPlayerError(): PlaybackException? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
if (playWhenReady) {
|
||||
} else {
|
||||
MPVLib.setPropertyBoolean("pause", true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean {
|
||||
val isPaused = MPVLib.getPropertyBoolean("pause")!!
|
||||
return !isPaused
|
||||
}
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) {
|
||||
}
|
||||
|
||||
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
||||
}
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading(): Boolean = false
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 10_000
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 30_000
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long = 10_000
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters {
|
||||
// TODO
|
||||
return PlaybackParameters.DEFAULT
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
pause()
|
||||
// MPVLib.setPropertyString("vo", "gpu")
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
MPVLib.destroy()
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
// TODO
|
||||
return Tracks.EMPTY
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||
// TODO
|
||||
return TrackSelectionParameters.Builder().build()
|
||||
}
|
||||
|
||||
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun getMediaMetadata(): MediaMetadata = mediaItem!!.mediaMetadata
|
||||
|
||||
override fun getPlaylistMetadata(): MediaMetadata = MediaMetadata.EMPTY
|
||||
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
// TODO
|
||||
return Timeline.EMPTY
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex(): Int {
|
||||
// TODO
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getCurrentMediaItemIndex(): Int = 0
|
||||
|
||||
override fun getDuration(): Long {
|
||||
val duration = MPVLib.getPropertyDouble("duration")!!.milliseconds.inWholeMilliseconds
|
||||
return duration
|
||||
}
|
||||
|
||||
override fun getCurrentPosition(): Long {
|
||||
val position = MPVLib.getPropertyDouble("time-pos/full")?.milliseconds?.inWholeMilliseconds ?: 0
|
||||
return position
|
||||
}
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
// TODO
|
||||
return currentPosition
|
||||
}
|
||||
|
||||
override fun getTotalBufferedDuration(): Long = bufferedPosition
|
||||
|
||||
override fun isPlayingAd(): Boolean = false
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getContentPosition(): Long = currentPosition
|
||||
|
||||
override fun getContentBufferedPosition(): Long = bufferedPosition
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
}
|
||||
|
||||
override fun getVolume(): Float = 1f
|
||||
|
||||
override fun clearVideoSurface(): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
val surface = surfaceView?.holder?.surface
|
||||
val newSurfaceSize = if (surface == null) 0 else C.LENGTH_UNSET
|
||||
this.surfaceSize = if (surface == null) Size(0, 0) else Size.UNKNOWN
|
||||
if (surface != null) {
|
||||
this.surface = surface
|
||||
MPVLib.attachSurface(surface)
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
|
||||
val url = mediaItem!!.localConfiguration?.uri.toString()
|
||||
MPVLib.command(arrayOf("loadfile", url))
|
||||
} else {
|
||||
clearVideoSurfaceView(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
MPVLib.detachSurface()
|
||||
MPVLib.setPropertyString("vo", "null")
|
||||
MPVLib.setPropertyString("force-window", "no")
|
||||
mediaItem = null
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
val width = MPVLib.getPropertyInt("width")
|
||||
val height = MPVLib.getPropertyInt("height")
|
||||
return if (width != null && height != null) {
|
||||
VideoSize(width, height)
|
||||
} else {
|
||||
VideoSize.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSurfaceSize(): Size {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
// TODO
|
||||
return DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build()
|
||||
}
|
||||
|
||||
override fun getDeviceVolume(): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isDeviceMuted(): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceVolume(
|
||||
volume: Int,
|
||||
flags: Int,
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(
|
||||
muted: Boolean,
|
||||
flags: Int,
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setAudioAttributes(
|
||||
audioAttributes: AudioAttributes,
|
||||
handleAudioFocus: Boolean,
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun seekTo(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int,
|
||||
isRepeatingCurrentItem: Boolean,
|
||||
) {
|
||||
if (mediaItemIndex == C.INDEX_UNSET) {
|
||||
return
|
||||
}
|
||||
MPVLib.setPropertyDouble("time-pos", positionMs.toDouble())
|
||||
}
|
||||
}
|
||||
82
app/src/main/jni/Android.mk
Normal file
82
app/src/main/jni/Android.mk
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
|
||||
PREFIX = $(PREFIX32)
|
||||
endif
|
||||
ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
|
||||
PREFIX = $(PREFIX64)
|
||||
endif
|
||||
ifeq ($(TARGET_ARCH_ABI),x86_64)
|
||||
PREFIX = $(PREFIX_X64)
|
||||
endif
|
||||
ifeq ($(TARGET_ARCH_ABI),x86)
|
||||
PREFIX = $(PREFIX_X86)
|
||||
endif
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libswresample
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libpostproc
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
# only include if library file exists
|
||||
ifneq (,$(wildcard $(LOCAL_SRC_FILES)))
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
endif
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavutil
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavcodec
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavformat
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libswscale
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavfilter
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavdevice
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(PREFIX)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libmpv
|
||||
LOCAL_SRC_FILES := $(PREFIX)/lib/libmpv.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(PREFIX)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := libplayer
|
||||
LOCAL_CFLAGS := -Werror
|
||||
LOCAL_CPPFLAGS += -std=c++11
|
||||
LOCAL_SRC_FILES := \
|
||||
main.cpp \
|
||||
render.cpp \
|
||||
log.cpp \
|
||||
jni_utils.cpp \
|
||||
property.cpp \
|
||||
event.cpp \
|
||||
thumbnail.cpp
|
||||
LOCAL_LDLIBS := -llog -lGLESv3 -lEGL -latomic
|
||||
LOCAL_SHARED_LIBRARIES := swscale avcodec mpv
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
17
app/src/main/jni/Application.mk
Normal file
17
app/src/main/jni/Application.mk
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
APP_ABI :=
|
||||
ifneq ($(PREFIX32),)
|
||||
APP_ABI += armeabi-v7a
|
||||
endif
|
||||
ifneq ($(PREFIX64),)
|
||||
APP_ABI += arm64-v8a
|
||||
endif
|
||||
ifneq ($(PREFIX_X64),)
|
||||
APP_ABI += x86_64
|
||||
endif
|
||||
ifneq ($(PREFIX_X86),)
|
||||
APP_ABI += x86
|
||||
endif
|
||||
|
||||
APP_PLATFORM := android-21
|
||||
APP_STL := c++_shared
|
||||
APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
|
||||
111
app/src/main/jni/event.cpp
Normal file
111
app/src/main/jni/event.cpp
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include "globals.h"
|
||||
#include "jni_utils.h"
|
||||
#include "log.h"
|
||||
|
||||
static void sendPropertyUpdateToJava(JNIEnv *env, mpv_event_property *prop)
|
||||
{
|
||||
jstring jprop = env->NewStringUTF(prop->name);
|
||||
jstring jvalue = NULL;
|
||||
switch (prop->format) {
|
||||
case MPV_FORMAT_NONE:
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_S, jprop);
|
||||
break;
|
||||
case MPV_FORMAT_FLAG:
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sb, jprop,
|
||||
(jboolean) (*(int*)prop->data != 0));
|
||||
break;
|
||||
case MPV_FORMAT_INT64:
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sl, jprop,
|
||||
(jlong) *(int64_t*)prop->data);
|
||||
break;
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sd, jprop,
|
||||
(jdouble) *(double*)prop->data);
|
||||
break;
|
||||
case MPV_FORMAT_STRING:
|
||||
jvalue = env->NewStringUTF(*(const char**)prop->data);
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_SS, jprop, jvalue);
|
||||
break;
|
||||
default:
|
||||
ALOGV("sendPropertyUpdateToJava: Unknown property update format received in callback: %d!", prop->format);
|
||||
break;
|
||||
}
|
||||
if (jprop)
|
||||
env->DeleteLocalRef(jprop);
|
||||
if (jvalue)
|
||||
env->DeleteLocalRef(jvalue);
|
||||
}
|
||||
|
||||
static void sendEventToJava(JNIEnv *env, int event)
|
||||
{
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_event, event);
|
||||
}
|
||||
|
||||
static void sendLogMessageToJava(JNIEnv *env, mpv_event_log_message *msg)
|
||||
{
|
||||
// filter the most obvious cases of invalid utf-8, since Java would choke on it
|
||||
const auto invalid_utf8 = [] (unsigned char c) {
|
||||
return c == 0xc0 || c == 0xc1 || c >= 0xf5;
|
||||
};
|
||||
for (int i = 0; msg->text[i]; i++) {
|
||||
if (invalid_utf8(static_cast<unsigned char>(msg->text[i])))
|
||||
return;
|
||||
}
|
||||
|
||||
jstring jprefix = env->NewStringUTF(msg->prefix);
|
||||
jstring jtext = env->NewStringUTF(msg->text);
|
||||
|
||||
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_logMessage_SiS,
|
||||
jprefix, (jint) msg->log_level, jtext);
|
||||
|
||||
if (jprefix)
|
||||
env->DeleteLocalRef(jprefix);
|
||||
if (jtext)
|
||||
env->DeleteLocalRef(jtext);
|
||||
}
|
||||
|
||||
void *event_thread(void *arg)
|
||||
{
|
||||
JNIEnv *env = NULL;
|
||||
acquire_jni_env(g_vm, &env);
|
||||
if (!env)
|
||||
die("failed to acquire java env");
|
||||
|
||||
while (1) {
|
||||
mpv_event *mp_event;
|
||||
mpv_event_property *mp_property = NULL;
|
||||
mpv_event_log_message *msg = NULL;
|
||||
|
||||
mp_event = mpv_wait_event(g_mpv, -1.0);
|
||||
|
||||
if (g_event_thread_request_exit)
|
||||
break;
|
||||
|
||||
if (mp_event->event_id == MPV_EVENT_NONE)
|
||||
continue;
|
||||
|
||||
switch (mp_event->event_id) {
|
||||
case MPV_EVENT_LOG_MESSAGE:
|
||||
msg = (mpv_event_log_message*)mp_event->data;
|
||||
ALOGV("[%s:%s] %s", msg->prefix, msg->level, msg->text);
|
||||
sendLogMessageToJava(env, msg);
|
||||
break;
|
||||
case MPV_EVENT_PROPERTY_CHANGE:
|
||||
mp_property = (mpv_event_property*)mp_event->data;
|
||||
sendPropertyUpdateToJava(env, mp_property);
|
||||
break;
|
||||
default:
|
||||
ALOGV("event: %s\n", mpv_event_name(mp_event->event_id));
|
||||
sendEventToJava(env, mp_event->event_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g_vm->DetachCurrentThread();
|
||||
|
||||
return NULL;
|
||||
}
|
||||
3
app/src/main/jni/event.h
Normal file
3
app/src/main/jni/event.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
void *event_thread(void *arg);
|
||||
7
app/src/main/jni/globals.h
Normal file
7
app/src/main/jni/globals.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
extern JavaVM *g_vm;
|
||||
extern mpv_handle *g_mpv;
|
||||
extern std::atomic<bool> g_event_thread_request_exit;
|
||||
51
app/src/main/jni/jni_utils.cpp
Normal file
51
app/src/main/jni/jni_utils.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#define UTIL_EXTERN
|
||||
#include "jni_utils.h"
|
||||
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
bool acquire_jni_env(JavaVM *vm, JNIEnv **env)
|
||||
{
|
||||
int ret = vm->GetEnv((void**) env, JNI_VERSION_1_6);
|
||||
if (ret == JNI_EDETACHED)
|
||||
return vm->AttachCurrentThread(env, NULL) == 0;
|
||||
else
|
||||
return ret == JNI_OK;
|
||||
}
|
||||
|
||||
// Apparently it's considered slow to FindClass and GetMethodID every time we need them,
|
||||
// so let's have a nice cache here.
|
||||
|
||||
void init_methods_cache(JNIEnv *env)
|
||||
{
|
||||
static bool methods_initialized = false;
|
||||
if (methods_initialized)
|
||||
return;
|
||||
|
||||
#define FIND_CLASS(name) reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(name)))
|
||||
java_Integer = FIND_CLASS("java/lang/Integer");
|
||||
java_Integer_init = env->GetMethodID(java_Integer, "<init>", "(I)V");
|
||||
java_Double = FIND_CLASS("java/lang/Double");
|
||||
java_Double_init = env->GetMethodID(java_Double, "<init>", "(D)V");
|
||||
java_Boolean = FIND_CLASS("java/lang/Boolean");
|
||||
java_Boolean_init = env->GetMethodID(java_Boolean, "<init>", "(Z)V");
|
||||
|
||||
android_graphics_Bitmap = FIND_CLASS("android/graphics/Bitmap");
|
||||
// createBitmap(int[], int, int, android.graphics.Bitmap$Config)
|
||||
android_graphics_Bitmap_createBitmap = env->GetStaticMethodID(android_graphics_Bitmap, "createBitmap", "([IIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
|
||||
android_graphics_Bitmap_Config = FIND_CLASS("android/graphics/Bitmap$Config");
|
||||
// static final android.graphics.Bitmap$Config ARGB_8888
|
||||
android_graphics_Bitmap_Config_ARGB_8888 = env->GetStaticFieldID(android_graphics_Bitmap_Config, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
|
||||
|
||||
mpv_MPVLib = FIND_CLASS("com/github/damontecres/wholphin/util/mpv/MPVLib");
|
||||
mpv_MPVLib_eventProperty_S = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;)V"); // eventProperty(String)
|
||||
mpv_MPVLib_eventProperty_Sb = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Z)V"); // eventProperty(String, boolean)
|
||||
mpv_MPVLib_eventProperty_Sl = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;J)V"); // eventProperty(String, long)
|
||||
mpv_MPVLib_eventProperty_Sd = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;D)V"); // eventProperty(String, double)
|
||||
mpv_MPVLib_eventProperty_SS = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Ljava/lang/String;)V"); // eventProperty(String, String)
|
||||
mpv_MPVLib_event = env->GetStaticMethodID(mpv_MPVLib, "event", "(I)V"); // event(int)
|
||||
mpv_MPVLib_logMessage_SiS = env->GetStaticMethodID(mpv_MPVLib, "logMessage", "(Ljava/lang/String;ILjava/lang/String;)V"); // logMessage(String, int, String)
|
||||
#undef FIND_CLASS
|
||||
|
||||
methods_initialized = true;
|
||||
}
|
||||
29
app/src/main/jni/jni_utils.h
Normal file
29
app/src/main/jni/jni_utils.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#define jni_func_name(name) Java_com_github_damontecres_wholphin_util_mpv_MPVLib_##name
|
||||
#define jni_func(return_type, name, ...) JNIEXPORT return_type JNICALL jni_func_name(name) (JNIEnv *env, jobject obj, ##__VA_ARGS__)
|
||||
|
||||
bool acquire_jni_env(JavaVM *vm, JNIEnv **env);
|
||||
void init_methods_cache(JNIEnv *env);
|
||||
|
||||
#ifndef UTIL_EXTERN
|
||||
#define UTIL_EXTERN extern
|
||||
#endif
|
||||
|
||||
UTIL_EXTERN jclass java_Integer, java_Double, java_Boolean;
|
||||
UTIL_EXTERN jmethodID java_Integer_init, java_Double_init, java_Boolean_init;
|
||||
|
||||
UTIL_EXTERN jclass android_graphics_Bitmap, android_graphics_Bitmap_Config;
|
||||
UTIL_EXTERN jmethodID android_graphics_Bitmap_createBitmap;
|
||||
UTIL_EXTERN jfieldID android_graphics_Bitmap_Config_ARGB_8888;
|
||||
|
||||
UTIL_EXTERN jclass mpv_MPVLib;
|
||||
UTIL_EXTERN jmethodID mpv_MPVLib_eventProperty_S,
|
||||
mpv_MPVLib_eventProperty_Sb,
|
||||
mpv_MPVLib_eventProperty_Sl,
|
||||
mpv_MPVLib_eventProperty_Sd,
|
||||
mpv_MPVLib_eventProperty_SS,
|
||||
mpv_MPVLib_event,
|
||||
mpv_MPVLib_logMessage_SiS;
|
||||
9
app/src/main/jni/log.cpp
Normal file
9
app/src/main/jni/log.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include "log.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
void die(const char *msg)
|
||||
{
|
||||
ALOGE("%s", msg);
|
||||
exit(1);
|
||||
}
|
||||
20
app/src/main/jni/log.h
Normal file
20
app/src/main/jni/log.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#define DEBUG 1
|
||||
|
||||
#define LOG_TAG "mpv"
|
||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#if DEBUG
|
||||
#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
|
||||
#else
|
||||
#define ALOGV(...) (void)0
|
||||
#endif
|
||||
|
||||
__attribute__((noreturn)) void die(const char *msg);
|
||||
|
||||
#define CHECK_MPV_INIT() do { \
|
||||
if (__builtin_expect(!g_mpv, 0)) \
|
||||
die("libmpv is not initialized"); \
|
||||
} while (0)
|
||||
108
app/src/main/jni/main.cpp
Normal file
108
app/src/main/jni/main.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <locale.h>
|
||||
#include <atomic>
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/jni.h>
|
||||
}
|
||||
|
||||
#include "log.h"
|
||||
#include "jni_utils.h"
|
||||
#include "event.h"
|
||||
|
||||
#define ARRAYLEN(a) (sizeof(a)/sizeof(a[0]))
|
||||
|
||||
extern "C" {
|
||||
jni_func(void, create, jobject appctx);
|
||||
jni_func(void, init);
|
||||
jni_func(void, destroy);
|
||||
|
||||
jni_func(void, command, jobjectArray jarray);
|
||||
};
|
||||
|
||||
JavaVM *g_vm;
|
||||
mpv_handle *g_mpv;
|
||||
std::atomic<bool> g_event_thread_request_exit(false);
|
||||
|
||||
static pthread_t event_thread_id;
|
||||
|
||||
static void prepare_environment(JNIEnv *env, jobject appctx) {
|
||||
setlocale(LC_NUMERIC, "C");
|
||||
|
||||
if (!env->GetJavaVM(&g_vm) && g_vm)
|
||||
av_jni_set_java_vm(g_vm, NULL);
|
||||
|
||||
jobject global_appctx = env->NewGlobalRef(appctx);
|
||||
if (global_appctx)
|
||||
av_jni_set_android_app_ctx(global_appctx, NULL);
|
||||
|
||||
init_methods_cache(env);
|
||||
}
|
||||
|
||||
jni_func(void, create, jobject appctx) {
|
||||
prepare_environment(env, appctx);
|
||||
|
||||
if (g_mpv)
|
||||
die("mpv is already initialized");
|
||||
|
||||
g_mpv = mpv_create();
|
||||
if (!g_mpv)
|
||||
die("context init failed");
|
||||
|
||||
// use terminal log level but request verbose messages
|
||||
// this way --msg-level can be used to adjust later
|
||||
mpv_request_log_messages(g_mpv, "terminal-default");
|
||||
mpv_set_option_string(g_mpv, "msg-level", "all=v");
|
||||
}
|
||||
|
||||
jni_func(void, init) {
|
||||
if (!g_mpv)
|
||||
die("mpv is not created");
|
||||
|
||||
if (mpv_initialize(g_mpv) < 0)
|
||||
die("mpv init failed");
|
||||
|
||||
g_event_thread_request_exit = false;
|
||||
if (pthread_create(&event_thread_id, NULL, event_thread, NULL) != 0)
|
||||
die("thread create failed");
|
||||
pthread_setname_np(event_thread_id, "event_thread");
|
||||
}
|
||||
|
||||
jni_func(void, destroy) {
|
||||
if (!g_mpv) {
|
||||
ALOGV("mpv destroy called but it's already destroyed");
|
||||
return;
|
||||
}
|
||||
|
||||
// poke event thread and wait for it to exit
|
||||
g_event_thread_request_exit = true;
|
||||
mpv_wakeup(g_mpv);
|
||||
pthread_join(event_thread_id, NULL);
|
||||
|
||||
mpv_terminate_destroy(g_mpv);
|
||||
g_mpv = NULL;
|
||||
}
|
||||
|
||||
jni_func(void, command, jobjectArray jarray) {
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
const char *arguments[128] = {0};
|
||||
int len = env->GetArrayLength(jarray);
|
||||
if (len >= ARRAYLEN(arguments))
|
||||
die("too many command arguments");
|
||||
|
||||
for (int i = 0; i < len; ++i)
|
||||
arguments[i] = env->GetStringUTFChars((jstring)env->GetObjectArrayElement(jarray, i), NULL);
|
||||
|
||||
mpv_command(g_mpv, arguments);
|
||||
|
||||
for (int i = 0; i < len; ++i)
|
||||
env->ReleaseStringUTFChars((jstring)env->GetObjectArrayElement(jarray, i), arguments[i]);
|
||||
}
|
||||
125
app/src/main/jni/property.cpp
Normal file
125
app/src/main/jni/property.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include "jni_utils.h"
|
||||
#include "log.h"
|
||||
#include "globals.h"
|
||||
|
||||
extern "C" {
|
||||
jni_func(jint, setOptionString, jstring option, jstring value);
|
||||
|
||||
jni_func(jobject, getPropertyInt, jstring property);
|
||||
jni_func(void, setPropertyInt, jstring property, jint value);
|
||||
jni_func(jobject, getPropertyDouble, jstring property);
|
||||
jni_func(void, setPropertyDouble, jstring property, jdouble value);
|
||||
jni_func(jobject, getPropertyBoolean, jstring property);
|
||||
jni_func(void, setPropertyBoolean, jstring property, jboolean value);
|
||||
jni_func(jstring, getPropertyString, jstring jproperty);
|
||||
jni_func(void, setPropertyString, jstring jproperty, jstring jvalue);
|
||||
|
||||
jni_func(void, observeProperty, jstring property, jint format);
|
||||
}
|
||||
|
||||
jni_func(jint, setOptionString, jstring joption, jstring jvalue) {
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
const char *option = env->GetStringUTFChars(joption, NULL);
|
||||
const char *value = env->GetStringUTFChars(jvalue, NULL);
|
||||
|
||||
int result = mpv_set_option_string(g_mpv, option, value);
|
||||
|
||||
env->ReleaseStringUTFChars(joption, option);
|
||||
env->ReleaseStringUTFChars(jvalue, value);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int common_get_property(JNIEnv *env, jstring jproperty, mpv_format format, void *output)
|
||||
{
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
const char *prop = env->GetStringUTFChars(jproperty, NULL);
|
||||
int result = mpv_get_property(g_mpv, prop, format, output);
|
||||
if (result == MPV_ERROR_PROPERTY_UNAVAILABLE)
|
||||
ALOGV("mpv_get_property(%s) format %d was unavailable", prop, format);
|
||||
else if (result < 0)
|
||||
ALOGE("mpv_get_property(%s) format %d returned error %s", prop, format, mpv_error_string(result));
|
||||
env->ReleaseStringUTFChars(jproperty, prop);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int common_set_property(JNIEnv *env, jstring jproperty, mpv_format format, void *value)
|
||||
{
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
const char *prop = env->GetStringUTFChars(jproperty, NULL);
|
||||
int result = mpv_set_property(g_mpv, prop, format, value);
|
||||
if (result < 0)
|
||||
ALOGE("mpv_set_property(%s, %p) format %d returned error %s", prop, value, format, mpv_error_string(result));
|
||||
env->ReleaseStringUTFChars(jproperty, prop);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
jni_func(jobject, getPropertyInt, jstring jproperty) {
|
||||
int64_t value = 0;
|
||||
if (common_get_property(env, jproperty, MPV_FORMAT_INT64, &value) < 0)
|
||||
return NULL;
|
||||
return env->NewObject(java_Integer, java_Integer_init, (jint)value);
|
||||
}
|
||||
|
||||
jni_func(jobject, getPropertyDouble, jstring jproperty) {
|
||||
double value = 0;
|
||||
if (common_get_property(env, jproperty, MPV_FORMAT_DOUBLE, &value) < 0)
|
||||
return NULL;
|
||||
return env->NewObject(java_Double, java_Double_init, (jdouble)value);
|
||||
}
|
||||
|
||||
jni_func(jobject, getPropertyBoolean, jstring jproperty) {
|
||||
int value = 0;
|
||||
if (common_get_property(env, jproperty, MPV_FORMAT_FLAG, &value) < 0)
|
||||
return NULL;
|
||||
return env->NewObject(java_Boolean, java_Boolean_init, (jboolean)value);
|
||||
}
|
||||
|
||||
jni_func(jstring, getPropertyString, jstring jproperty) {
|
||||
char *value;
|
||||
if (common_get_property(env, jproperty, MPV_FORMAT_STRING, &value) < 0)
|
||||
return NULL;
|
||||
jstring jvalue = env->NewStringUTF(value);
|
||||
mpv_free(value);
|
||||
return jvalue;
|
||||
}
|
||||
|
||||
jni_func(void, setPropertyInt, jstring jproperty, jint jvalue) {
|
||||
int64_t value = static_cast<int64_t>(jvalue);
|
||||
common_set_property(env, jproperty, MPV_FORMAT_INT64, &value);
|
||||
}
|
||||
|
||||
jni_func(void, setPropertyDouble, jstring jproperty, jdouble jvalue) {
|
||||
double value = static_cast<double>(jvalue);
|
||||
common_set_property(env, jproperty, MPV_FORMAT_DOUBLE, &value);
|
||||
}
|
||||
|
||||
jni_func(void, setPropertyBoolean, jstring jproperty, jboolean jvalue) {
|
||||
int value = jvalue == JNI_TRUE ? 1 : 0;
|
||||
common_set_property(env, jproperty, MPV_FORMAT_FLAG, &value);
|
||||
}
|
||||
|
||||
jni_func(void, setPropertyString, jstring jproperty, jstring jvalue) {
|
||||
const char *value = env->GetStringUTFChars(jvalue, NULL);
|
||||
common_set_property(env, jproperty, MPV_FORMAT_STRING, &value);
|
||||
env->ReleaseStringUTFChars(jvalue, value);
|
||||
}
|
||||
|
||||
jni_func(void, observeProperty, jstring property, jint format) {
|
||||
CHECK_MPV_INIT();
|
||||
const char *prop = env->GetStringUTFChars(property, NULL);
|
||||
int result = mpv_observe_property(g_mpv, 0, prop, (mpv_format)format);
|
||||
if (result < 0)
|
||||
ALOGE("mpv_observe_property(%s) format %d returned error %s", prop, format, mpv_error_string(result));
|
||||
env->ReleaseStringUTFChars(property, prop);
|
||||
}
|
||||
38
app/src/main/jni/render.cpp
Normal file
38
app/src/main/jni/render.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include "jni_utils.h"
|
||||
#include "log.h"
|
||||
#include "globals.h"
|
||||
|
||||
extern "C" {
|
||||
jni_func(void, attachSurface, jobject surface_);
|
||||
jni_func(void, detachSurface);
|
||||
};
|
||||
|
||||
static jobject surface;
|
||||
|
||||
jni_func(void, attachSurface, jobject surface_) {
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
surface = env->NewGlobalRef(surface_);
|
||||
if (!surface)
|
||||
die("invalid surface provided");
|
||||
int64_t wid = reinterpret_cast<intptr_t>(surface);
|
||||
int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid);
|
||||
if (result < 0)
|
||||
ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result));
|
||||
}
|
||||
|
||||
jni_func(void, detachSurface) {
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
int64_t wid = 0;
|
||||
int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid);
|
||||
if (result < 0)
|
||||
ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result));
|
||||
|
||||
env->DeleteGlobalRef(surface);
|
||||
surface = NULL;
|
||||
}
|
||||
133
app/src/main/jni/thumbnail.cpp
Normal file
133
app/src/main/jni/thumbnail.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <mpv/client.h>
|
||||
|
||||
extern "C" {
|
||||
#include <libswscale/swscale.h>
|
||||
};
|
||||
|
||||
#include "jni_utils.h"
|
||||
#include "globals.h"
|
||||
#include "log.h"
|
||||
|
||||
extern "C" {
|
||||
jni_func(jobject, grabThumbnail, jint dimension);
|
||||
};
|
||||
|
||||
static inline mpv_node make_node_str(const char *s)
|
||||
{
|
||||
mpv_node r{};
|
||||
r.format = MPV_FORMAT_STRING;
|
||||
r.u.string = const_cast<char*>(s);
|
||||
return r;
|
||||
}
|
||||
|
||||
jni_func(jobject, grabThumbnail, jint dimension) {
|
||||
CHECK_MPV_INIT();
|
||||
|
||||
mpv_node result{};
|
||||
{
|
||||
mpv_node c{}, c_args[2];
|
||||
mpv_node_list c_array{};
|
||||
c_args[0] = make_node_str("screenshot-raw");
|
||||
c_args[1] = make_node_str("video");
|
||||
c_array.num = 2;
|
||||
c_array.values = c_args;
|
||||
c.format = MPV_FORMAT_NODE_ARRAY;
|
||||
c.u.list = &c_array;
|
||||
if (mpv_command_node(g_mpv, &c, &result) < 0) {
|
||||
ALOGE("screenshot-raw command failed");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// extract relevant property data from the node map mpv returns
|
||||
int w = 0, h = 0, stride = 0;
|
||||
bool format_ok = false;
|
||||
struct mpv_byte_array *data = NULL;
|
||||
do {
|
||||
if (result.format != MPV_FORMAT_NODE_MAP)
|
||||
break;
|
||||
for (int i = 0; i < result.u.list->num; i++) {
|
||||
std::string key(result.u.list->keys[i]);
|
||||
const mpv_node *val = &result.u.list->values[i];
|
||||
if (key == "w" || key == "h" || key == "stride") {
|
||||
if (val->format != MPV_FORMAT_INT64)
|
||||
break;
|
||||
if (key == "w")
|
||||
w = val->u.int64;
|
||||
else if (key == "h")
|
||||
h = val->u.int64;
|
||||
else
|
||||
stride = val->u.int64;
|
||||
} else if (key == "format") {
|
||||
if (val->format != MPV_FORMAT_STRING)
|
||||
break;
|
||||
format_ok = !strcmp(val->u.string, "bgr0");
|
||||
} else if (key == "data") {
|
||||
if (val->format != MPV_FORMAT_BYTE_ARRAY)
|
||||
break;
|
||||
data = val->u.ba;
|
||||
}
|
||||
}
|
||||
} while (0);
|
||||
if (!w || !h || !stride || !format_ok || !data) {
|
||||
ALOGE("extracting data failed");
|
||||
mpv_free_node_contents(&result);
|
||||
return NULL;
|
||||
}
|
||||
ALOGV("screenshot w:%d h:%d stride:%d", w, h, stride);
|
||||
|
||||
// crop to square
|
||||
int crop_left = 0, crop_top = 0;
|
||||
int new_w = w, new_h = h;
|
||||
if (w > h) {
|
||||
crop_left = (w - h) / 2;
|
||||
new_w = h;
|
||||
} else {
|
||||
crop_top = (h - w) / 2;
|
||||
new_h = w;
|
||||
}
|
||||
ALOGV("cropped w:%u h:%u", new_w, new_h);
|
||||
|
||||
uint8_t *new_data = reinterpret_cast<uint8_t*>(data->data);
|
||||
new_data += crop_left * sizeof(uint32_t); // move begin rightwards
|
||||
new_data += stride * crop_top; // move begin downwards
|
||||
|
||||
// convert & scale to appropriate size
|
||||
struct SwsContext *ctx = sws_getContext(
|
||||
new_w, new_h, AV_PIX_FMT_BGR0,
|
||||
dimension, dimension, AV_PIX_FMT_RGB32,
|
||||
SWS_BICUBIC, NULL, NULL, NULL);
|
||||
if (!ctx) {
|
||||
mpv_free_node_contents(&result);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jintArray arr = env->NewIntArray(dimension * dimension);
|
||||
jint *scaled = env->GetIntArrayElements(arr, NULL);
|
||||
|
||||
uint8_t *src_p[4] = { new_data }, *dst_p[4] = { (uint8_t*) scaled };
|
||||
int src_stride[4] = { stride },
|
||||
dst_stride[4] = { (int) sizeof(jint) * dimension };
|
||||
sws_scale(ctx, src_p, src_stride, 0, new_h, dst_p, dst_stride);
|
||||
sws_freeContext(ctx);
|
||||
|
||||
mpv_free_node_contents(&result); // frees data->data
|
||||
|
||||
// create android.graphics.Bitmap
|
||||
env->ReleaseIntArrayElements(arr, scaled, 0);
|
||||
|
||||
jobject bitmap_config =
|
||||
env->GetStaticObjectField(android_graphics_Bitmap_Config, android_graphics_Bitmap_Config_ARGB_8888);
|
||||
jobject bitmap =
|
||||
env->CallStaticObjectMethod(android_graphics_Bitmap, android_graphics_Bitmap_createBitmap,
|
||||
arr, dimension, dimension, bitmap_config);
|
||||
env->DeleteLocalRef(arr);
|
||||
env->DeleteLocalRef(bitmap_config);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue