config added mpv files after prebuild

This commit is contained in:
tapframe 2025-12-23 17:48:52 +05:30
parent 18fa11fd88
commit 811701ebae
7 changed files with 731 additions and 4 deletions

View file

@ -97,7 +97,8 @@
"receiverAppId": "CC1AD845",
"iosStartDiscoveryAfterFirstTapOnCastButton": true
}
]
],
"./plugins/mpv-bridge/withMpvBridge"
],
"updates": {
"enabled": true,

View file

@ -475,7 +475,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = "Nuvio";
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -506,8 +506,8 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
PRODUCT_NAME = "Nuvio";
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View file

@ -213,6 +213,8 @@ PODS:
- ExpoModulesCore
- ExpoBrightness (14.0.8):
- ExpoModulesCore
- ExpoClipboard (8.0.8):
- ExpoModulesCore
- ExpoCrypto (15.0.8):
- ExpoModulesCore
- ExpoDevice (8.0.10):
@ -2749,6 +2751,7 @@ DEPENDENCIES:
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoBlur (from `../node_modules/expo-blur/ios`)
- ExpoBrightness (from `../node_modules/expo-brightness/ios`)
- ExpoClipboard (from `../node_modules/expo-clipboard/ios`)
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
- ExpoDevice (from `../node_modules/expo-device/ios`)
- ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`)
@ -2912,6 +2915,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-blur/ios"
ExpoBrightness:
:path: "../node_modules/expo-brightness/ios"
ExpoClipboard:
:path: "../node_modules/expo-clipboard/ios"
ExpoCrypto:
:path: "../node_modules/expo-crypto/ios"
ExpoDevice:
@ -3171,6 +3176,7 @@ SPEC CHECKSUMS:
ExpoAsset: 23a958e97d3d340919fe6774db35d563241e6c03
ExpoBlur: b90747a3f22a8b6ceffd9cb0dc41a4184efdc656
ExpoBrightness: 46c980463e8a54b9ce77f923c4bff0bb0c9526e0
ExpoClipboard: b36b287d8356887844bb08ed5c84b5979bb4dd1e
ExpoCrypto: b6105ebaa15d6b38a811e71e43b52cd934945322
ExpoDevice: 6327c3c200816795708885adf540d26ecab83d1a
ExpoDocumentPicker: 7cd9e71a0f66fb19eb0a586d6f26eee1284692e0

View file

@ -0,0 +1,427 @@
package com.nuvio.app.mpv
import android.content.Context
import android.graphics.SurfaceTexture
import android.util.AttributeSet
import android.util.Log
import android.view.Surface
import android.view.TextureView
import dev.jdtech.mpv.MPVLib
class MPVView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextureView(context, attrs, defStyleAttr), TextureView.SurfaceTextureListener, MPVLib.EventObserver {
companion object {
private const val TAG = "MPVView"
}
private var isMpvInitialized = false
private var pendingDataSource: String? = null
private var isPaused: Boolean = true
private var surface: Surface? = null
private var httpHeaders: Map<String, String>? = null
// Event listener for React Native
var onLoadCallback: ((duration: Double, width: Int, height: Int) -> Unit)? = null
var onProgressCallback: ((position: Double, duration: Double) -> Unit)? = null
var onEndCallback: (() -> Unit)? = null
var onErrorCallback: ((message: String) -> Unit)? = null
var onTracksChangedCallback: ((audioTracks: List<Map<String, Any>>, subtitleTracks: List<Map<String, Any>>) -> Unit)? = null
init {
surfaceTextureListener = this
isOpaque = false
}
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG, "Surface texture available: ${width}x${height}")
try {
surface = Surface(surfaceTexture)
MPVLib.create(context.applicationContext)
initOptions()
MPVLib.init()
MPVLib.attachSurface(surface!!)
MPVLib.addObserver(this)
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
observeProperties()
isMpvInitialized = true
// If a data source was set before surface was ready, load it now
pendingDataSource?.let { url ->
applyHttpHeaders()
loadFile(url)
pendingDataSource = null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize MPV", e)
onErrorCallback?.invoke("MPV initialization failed: ${e.message}")
}
}
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG, "Surface texture size changed: ${width}x${height}")
if (isMpvInitialized) {
MPVLib.setPropertyString("android-surface-size", "${width}x${height}")
}
}
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
Log.d(TAG, "Surface texture destroyed")
if (isMpvInitialized) {
MPVLib.removeObserver(this)
MPVLib.detachSurface()
MPVLib.destroy()
isMpvInitialized = false
}
surface?.release()
surface = null
return true
}
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
// Called when the SurfaceTexture is updated via updateTexImage()
}
private fun initOptions() {
// Mobile-optimized profile
MPVLib.setOptionString("profile", "fast")
MPVLib.setOptionString("vo", "gpu")
MPVLib.setOptionString("gpu-context", "android")
MPVLib.setOptionString("opengl-es", "yes")
// Hardware decoding - use mediacodec-copy to allow subtitle overlay
// 'mediacodec-copy' copies frames to CPU memory which enables subtitle blending
MPVLib.setOptionString("hwdec", "auto")
MPVLib.setOptionString("hwdec-codecs", "all")
// Audio output
MPVLib.setOptionString("ao", "audiotrack,opensles")
// Network caching for streaming
MPVLib.setOptionString("demuxer-max-bytes", "67108864") // 64MB
MPVLib.setOptionString("demuxer-max-back-bytes", "33554432") // 32MB
MPVLib.setOptionString("cache", "yes")
MPVLib.setOptionString("cache-secs", "30")
// Network options
MPVLib.setOptionString("network-timeout", "60") // 60 second timeout
// Subtitle configuration - CRITICAL for Android
MPVLib.setOptionString("sub-auto", "fuzzy") // Auto-load subtitles
MPVLib.setOptionString("sub-visibility", "yes") // Make subtitles visible by default
MPVLib.setOptionString("sub-font-size", "48") // Larger font size for mobile readability
MPVLib.setOptionString("sub-pos", "95") // Position at bottom (0-100, 100 = very bottom)
MPVLib.setOptionString("sub-color", "#FFFFFFFF") // White color
MPVLib.setOptionString("sub-border-size", "3") // Thicker border for readability
MPVLib.setOptionString("sub-border-color", "#FF000000") // Black border
MPVLib.setOptionString("sub-shadow-offset", "2") // Add shadow for better visibility
MPVLib.setOptionString("sub-shadow-color", "#80000000") // Semi-transparent black shadow
// Font configuration - point to Android system fonts for all language support
MPVLib.setOptionString("osd-fonts-dir", "/system/fonts")
MPVLib.setOptionString("sub-fonts-dir", "/system/fonts")
MPVLib.setOptionString("sub-font", "Roboto") // Default fallback font
// Allow embedded fonts in ASS/SSA but fallback to system fonts
MPVLib.setOptionString("embeddedfonts", "yes")
// Language/encoding support for various subtitle formats
MPVLib.setOptionString("sub-codepage", "auto") // Auto-detect encoding (supports UTF-8, Latin, CJK, etc.)
MPVLib.setOptionString("osc", "no") // Disable on screen controller
MPVLib.setOptionString("osd-level", "1")
// Critical for subtitle rendering on Android GPU
// blend-subtitles=no lets the GPU renderer handle subtitle overlay properly
MPVLib.setOptionString("blend-subtitles", "no")
MPVLib.setOptionString("sub-use-margins", "no")
// Use 'scale' to allow ASS styling but with our scale and font overrides
// This preserves styled subtitles while having font fallbacks
MPVLib.setOptionString("sub-ass-override", "scale")
MPVLib.setOptionString("sub-scale", "1.0")
MPVLib.setOptionString("sub-fix-timing", "yes") // Fix timing for SRT subtitles
// Force subtitle rendering
MPVLib.setOptionString("sid", "auto") // Auto-select subtitle track
// Disable terminal/input
MPVLib.setOptionString("terminal", "no")
MPVLib.setOptionString("input-default-bindings", "no")
}
private fun observeProperties() {
// MPV format constants (from MPVLib source)
val MPV_FORMAT_NONE = 0
val MPV_FORMAT_FLAG = 3
val MPV_FORMAT_INT64 = 4
val MPV_FORMAT_DOUBLE = 5
MPVLib.observeProperty("time-pos", MPV_FORMAT_DOUBLE)
MPVLib.observeProperty("duration/full", MPV_FORMAT_DOUBLE) // Use /full for complete HLS duration
MPVLib.observeProperty("pause", MPV_FORMAT_FLAG)
MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG)
MPVLib.observeProperty("eof-reached", MPV_FORMAT_FLAG)
MPVLib.observeProperty("video-params/aspect", MPV_FORMAT_DOUBLE)
MPVLib.observeProperty("width", MPV_FORMAT_INT64)
MPVLib.observeProperty("height", MPV_FORMAT_INT64)
MPVLib.observeProperty("track-list", MPV_FORMAT_NONE)
// Observe subtitle properties for debugging
MPVLib.observeProperty("sid", MPV_FORMAT_INT64)
MPVLib.observeProperty("sub-visibility", MPV_FORMAT_FLAG)
MPVLib.observeProperty("sub-text", MPV_FORMAT_NONE)
}
private fun loadFile(url: String) {
Log.d(TAG, "Loading file: $url")
MPVLib.command(arrayOf("loadfile", url))
}
// Public API
fun setDataSource(url: String) {
if (isMpvInitialized) {
// Apply headers before loading the file
applyHttpHeaders()
loadFile(url)
} else {
pendingDataSource = url
}
}
fun setHeaders(headers: Map<String, String>?) {
httpHeaders = headers
Log.d(TAG, "Headers set: $headers")
}
private fun applyHttpHeaders() {
httpHeaders?.let { headers ->
if (headers.isNotEmpty()) {
// Format headers for MPV: comma-separated "Key: Value" pairs
val headerList = headers.map { (key, value) -> "$key: $value" }
val headerString = headerList.joinToString(",")
Log.d(TAG, "Applying HTTP headers: $headerString")
MPVLib.setOptionString("http-header-fields", headerString)
}
}
}
fun setPaused(paused: Boolean) {
isPaused = paused
if (isMpvInitialized) {
MPVLib.setPropertyBoolean("pause", paused)
}
}
fun seekTo(positionSeconds: Double) {
Log.d(TAG, "seekTo called: positionSeconds=$positionSeconds, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
Log.d(TAG, "Executing MPV seek command: seek $positionSeconds absolute")
MPVLib.command(arrayOf("seek", positionSeconds.toString(), "absolute"))
}
}
fun setSpeed(speed: Double) {
if (isMpvInitialized) {
MPVLib.setPropertyDouble("speed", speed)
}
}
fun setVolume(volume: Double) {
if (isMpvInitialized) {
// MPV volume is 0-100
MPVLib.setPropertyDouble("volume", volume * 100.0)
}
}
fun setAudioTrack(trackId: Int) {
if (isMpvInitialized) {
if (trackId == -1) {
MPVLib.setPropertyString("aid", "no")
} else {
MPVLib.setPropertyInt("aid", trackId)
}
}
}
fun setSubtitleTrack(trackId: Int) {
Log.d(TAG, "setSubtitleTrack called: trackId=$trackId, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
if (trackId == -1) {
Log.d(TAG, "Disabling subtitles (sid=no)")
MPVLib.setPropertyString("sid", "no")
MPVLib.setPropertyString("sub-visibility", "no")
} else {
Log.d(TAG, "Setting subtitle track to: $trackId")
MPVLib.setPropertyInt("sid", trackId)
// Ensure subtitles are visible
MPVLib.setPropertyString("sub-visibility", "yes")
// Debug: Verify the subtitle was set correctly
val currentSid = MPVLib.getPropertyInt("sid")
val subVisibility = MPVLib.getPropertyString("sub-visibility")
val subDelay = MPVLib.getPropertyDouble("sub-delay")
val subScale = MPVLib.getPropertyDouble("sub-scale")
Log.d(TAG, "After setting - sid=$currentSid, sub-visibility=$subVisibility, sub-delay=$subDelay, sub-scale=$subScale")
}
}
}
fun setResizeMode(mode: String) {
Log.d(TAG, "setResizeMode called: mode=$mode, isMpvInitialized=$isMpvInitialized")
if (isMpvInitialized) {
when (mode) {
"contain" -> {
// Letterbox - show entire video with black bars
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
"cover" -> {
// Fill/crop - zoom to fill, cropping edges
MPVLib.setPropertyDouble("panscan", 1.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
"stretch" -> {
// Stretch - disable aspect ratio
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "no")
}
else -> {
// Default to contain
MPVLib.setPropertyDouble("panscan", 0.0)
MPVLib.setPropertyString("keepaspect", "yes")
}
}
}
}
// MPVLib.EventObserver implementation
override fun eventProperty(property: String) {
Log.d(TAG, "Property changed: $property")
when (property) {
"track-list" -> {
// Parse track list and notify React Native
parseAndSendTracks()
}
}
}
private fun parseAndSendTracks() {
try {
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: 0
Log.d(TAG, "Track count: $trackCount")
val audioTracks = mutableListOf<Map<String, Any>>()
val subtitleTracks = mutableListOf<Map<String, Any>>()
for (i in 0 until trackCount) {
val type = MPVLib.getPropertyString("track-list/$i/type") ?: continue
val id = MPVLib.getPropertyInt("track-list/$i/id") ?: continue
val title = MPVLib.getPropertyString("track-list/$i/title") ?: ""
val lang = MPVLib.getPropertyString("track-list/$i/lang") ?: ""
val codec = MPVLib.getPropertyString("track-list/$i/codec") ?: ""
val trackName = when {
title.isNotEmpty() -> title
lang.isNotEmpty() -> lang.uppercase()
else -> "Track $id"
}
val track = mapOf(
"id" to id,
"name" to trackName,
"language" to lang,
"codec" to codec
)
when (type) {
"audio" -> {
Log.d(TAG, "Found audio track: $track")
audioTracks.add(track)
}
"sub" -> {
Log.d(TAG, "Found subtitle track: $track")
subtitleTracks.add(track)
}
}
}
Log.d(TAG, "Sending tracks - Audio: ${audioTracks.size}, Subtitles: ${subtitleTracks.size}")
onTracksChangedCallback?.invoke(audioTracks, subtitleTracks)
} catch (e: Exception) {
Log.e(TAG, "Error parsing tracks", e)
}
}
override fun eventProperty(property: String, value: Long) {
Log.d(TAG, "Property $property = $value (Long)")
}
override fun eventProperty(property: String, value: Double) {
Log.d(TAG, "Property $property = $value (Double)")
when (property) {
"time-pos" -> {
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
onProgressCallback?.invoke(value, duration)
}
"duration/full", "duration" -> {
val width = MPVLib.getPropertyInt("width") ?: 0
val height = MPVLib.getPropertyInt("height") ?: 0
onLoadCallback?.invoke(value, width, height)
}
}
}
override fun eventProperty(property: String, value: Boolean) {
Log.d(TAG, "Property $property = $value (Boolean)")
when (property) {
"eof-reached" -> {
if (value) {
onEndCallback?.invoke()
}
}
}
}
override fun eventProperty(property: String, value: String) {
Log.d(TAG, "Property $property = $value (String)")
}
override fun event(eventId: Int) {
Log.d(TAG, "Event: $eventId")
// MPV event constants (from MPVLib source)
val MPV_EVENT_FILE_LOADED = 8
val MPV_EVENT_END_FILE = 7
when (eventId) {
MPV_EVENT_FILE_LOADED -> {
// File is loaded, start playback if not paused
if (!isPaused) {
MPVLib.setPropertyBoolean("pause", false)
}
}
MPV_EVENT_END_FILE -> {
Log.d(TAG, "MPV_EVENT_END_FILE")
// Heuristic: If duration is effectively 0 at end of file, it's a load error
val duration = MPVLib.getPropertyDouble("duration/full") ?: MPVLib.getPropertyDouble("duration") ?: 0.0
val timePos = MPVLib.getPropertyDouble("time-pos") ?: 0.0
val eofReached = MPVLib.getPropertyBoolean("eof-reached") ?: false
Log.d(TAG, "End stats - Duration: $duration, Time: $timePos, EOF: $eofReached")
if (duration < 1.0 && !eofReached) {
val customError = "Unable to play media. Source may be unreachable."
Log.e(TAG, "Playback error detected (heuristic): $customError")
onErrorCallback?.invoke(customError)
} else {
onEndCallback?.invoke()
}
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.nuvio.app.mpv
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class MpvPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(MpvPlayerViewManager(reactContext))
}
}

View file

@ -0,0 +1,183 @@
package com.nuvio.app.mpv
import android.graphics.Color
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.events.RCTEventEmitter
class MpvPlayerViewManager(
private val reactContext: ReactApplicationContext
) : SimpleViewManager<MPVView>() {
companion object {
const val REACT_CLASS = "MpvPlayer"
// Commands
const val COMMAND_SEEK = 1
const val COMMAND_SET_AUDIO_TRACK = 2
const val COMMAND_SET_SUBTITLE_TRACK = 3
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(context: ThemedReactContext): MPVView {
val view = MPVView(context)
// Note: Do NOT set background color - it will block the SurfaceView content
// Set up event callbacks
view.onLoadCallback = { duration, width, height ->
val event = Arguments.createMap().apply {
putDouble("duration", duration)
putInt("width", width)
putInt("height", height)
}
sendEvent(context, view.id, "onLoad", event)
}
view.onProgressCallback = { position, duration ->
val event = Arguments.createMap().apply {
putDouble("currentTime", position)
putDouble("duration", duration)
}
sendEvent(context, view.id, "onProgress", event)
}
view.onEndCallback = {
sendEvent(context, view.id, "onEnd", Arguments.createMap())
}
view.onErrorCallback = { message ->
val event = Arguments.createMap().apply {
putString("error", message)
}
sendEvent(context, view.id, "onError", event)
}
view.onTracksChangedCallback = { audioTracks, subtitleTracks ->
val event = Arguments.createMap().apply {
val audioArray = Arguments.createArray()
audioTracks.forEach { track ->
val trackMap = Arguments.createMap().apply {
putInt("id", track["id"] as Int)
putString("name", track["name"] as String)
putString("language", track["language"] as String)
putString("codec", track["codec"] as String)
}
audioArray.pushMap(trackMap)
}
putArray("audioTracks", audioArray)
val subtitleArray = Arguments.createArray()
subtitleTracks.forEach { track ->
val trackMap = Arguments.createMap().apply {
putInt("id", track["id"] as Int)
putString("name", track["name"] as String)
putString("language", track["language"] as String)
putString("codec", track["codec"] as String)
}
subtitleArray.pushMap(trackMap)
}
putArray("subtitleTracks", subtitleArray)
}
sendEvent(context, view.id, "onTracksChanged", event)
}
return view
}
private fun sendEvent(context: ThemedReactContext, viewId: Int, eventName: String, params: com.facebook.react.bridge.WritableMap) {
context.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(viewId, eventName, params)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return MapBuilder.builder<String, Any>()
.put("onLoad", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onLoad")))
.put("onProgress", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onProgress")))
.put("onEnd", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onEnd")))
.put("onError", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onError")))
.put("onTracksChanged", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onTracksChanged")))
.build()
}
override fun getCommandsMap(): Map<String, Int> {
return MapBuilder.of(
"seek", COMMAND_SEEK,
"setAudioTrack", COMMAND_SET_AUDIO_TRACK,
"setSubtitleTrack", COMMAND_SET_SUBTITLE_TRACK
)
}
override fun receiveCommand(view: MPVView, commandId: String?, args: ReadableArray?) {
android.util.Log.d("MpvPlayerViewManager", "receiveCommand: $commandId, args: $args")
when (commandId) {
"seek" -> {
val position = args?.getDouble(0)
android.util.Log.d("MpvPlayerViewManager", "Seek command received: position=$position")
position?.let { view.seekTo(it) }
}
"setAudioTrack" -> {
args?.getInt(0)?.let { view.setAudioTrack(it) }
}
"setSubtitleTrack" -> {
args?.getInt(0)?.let { view.setSubtitleTrack(it) }
}
}
}
// React Props
@ReactProp(name = "source")
fun setSource(view: MPVView, source: String?) {
source?.let { view.setDataSource(it) }
}
@ReactProp(name = "paused")
fun setPaused(view: MPVView, paused: Boolean) {
view.setPaused(paused)
}
@ReactProp(name = "volume", defaultFloat = 1.0f)
fun setVolume(view: MPVView, volume: Float) {
view.setVolume(volume.toDouble())
}
@ReactProp(name = "rate", defaultFloat = 1.0f)
fun setRate(view: MPVView, rate: Float) {
view.setSpeed(rate.toDouble())
}
// Handle backgroundColor prop to prevent crash from React Native style system
@ReactProp(name = "backgroundColor", customType = "Color")
fun setBackgroundColor(view: MPVView, color: Int?) {
// Intentionally ignoring - background color would block the TextureView content
// Leave the view transparent
}
@ReactProp(name = "resizeMode")
fun setResizeMode(view: MPVView, resizeMode: String?) {
view.setResizeMode(resizeMode ?: "contain")
}
@ReactProp(name = "headers")
fun setHeaders(view: MPVView, headers: com.facebook.react.bridge.ReadableMap?) {
if (headers != null) {
val headerMap = mutableMapOf<String, String>()
val iterator = headers.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
headers.getString(key)?.let { value ->
headerMap[key] = value
}
}
view.setHeaders(headerMap)
} else {
view.setHeaders(null)
}
}
}

View file

@ -0,0 +1,94 @@
const { withDangerousMod, withMainApplication, withMainActivity } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
/**
* Copy MPV native files to android project
*/
function copyMpvFiles(projectRoot) {
const sourceDir = path.join(projectRoot, 'plugins', 'mpv-bridge', 'android', 'mpv');
const destDir = path.join(projectRoot, 'android', 'app', 'src', 'main', 'java', 'com', 'nuvio', 'app', 'mpv');
// Create destination directory if it doesn't exist
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
// Copy all files from source to destination
if (fs.existsSync(sourceDir)) {
const files = fs.readdirSync(sourceDir);
files.forEach(file => {
const srcFile = path.join(sourceDir, file);
const destFile = path.join(destDir, file);
if (fs.statSync(srcFile).isFile()) {
fs.copyFileSync(srcFile, destFile);
console.log(`[mpv-bridge] Copied ${file} to android project`);
}
});
}
}
/**
* Modify MainApplication.kt to include MpvPackage
*/
function withMpvMainApplication(config) {
return withMainApplication(config, async (config) => {
let contents = config.modResults.contents;
// Add import for MpvPackage
const mpvImport = 'import com.nuvio.app.mpv.MpvPackage';
if (!contents.includes(mpvImport)) {
// Add import after the last import statement
const lastImportIndex = contents.lastIndexOf('import ');
const endOfLastImport = contents.indexOf('\n', lastImportIndex);
contents = contents.slice(0, endOfLastImport + 1) + mpvImport + '\n' + contents.slice(endOfLastImport + 1);
}
// Add MpvPackage to the packages list
const packagesPattern = /override fun getPackages\(\): List<ReactPackage> \{[\s\S]*?return PackageList\(this\)\.packages\.apply \{/;
if (contents.match(packagesPattern) && !contents.includes('MpvPackage()')) {
contents = contents.replace(
packagesPattern,
(match) => match + '\n add(MpvPackage())'
);
}
config.modResults.contents = contents;
return config;
});
}
/**
* Modify MainActivity.kt to handle MPV lifecycle if needed
*/
function withMpvMainActivity(config) {
return withMainActivity(config, async (config) => {
// Currently no modifications needed for MainActivity
// But this hook is available for future enhancements
return config;
});
}
/**
* Main plugin function
*/
function withMpvBridge(config) {
// Copy native files during prebuild
config = withDangerousMod(config, [
'android',
async (config) => {
copyMpvFiles(config.modRequest.projectRoot);
return config;
},
]);
// Modify MainApplication to register the package
config = withMpvMainApplication(config);
// Modify MainActivity if needed
config = withMpvMainActivity(config);
return config;
}
module.exports = withMpvBridge;