This commit is contained in:
Damontecres 2025-11-05 09:23:27 -05:00
parent 77c787b51f
commit 1ee834d42b
No known key found for this signature in database
37 changed files with 2322 additions and 18 deletions

View file

@ -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)

View file

@ -236,7 +236,7 @@ fun PlaybackPage(
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier,
)
if (presentationState.coverSurface) {
if (false && presentationState.coverSurface) {
Box(
Modifier
.matchParentSize()

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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())
}
}

View 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)

View 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
View 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
View file

@ -0,0 +1,3 @@
#pragma once
void *event_thread(void *arg);

View 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;

View 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;
}

View 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
View 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
View 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
View 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]);
}

View 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);
}

View 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;
}

View 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;
}