mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
config added mpv files after prebuild
This commit is contained in:
parent
18fa11fd88
commit
811701ebae
7 changed files with 731 additions and 4 deletions
3
app.json
3
app.json
|
|
@ -97,7 +97,8 @@
|
|||
"receiverAppId": "CC1AD845",
|
||||
"iosStartDiscoveryAfterFirstTapOnCastButton": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"./plugins/mpv-bridge/withMpvBridge"
|
||||
],
|
||||
"updates": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
427
plugins/mpv-bridge/android/mpv/MPVView.kt
Normal file
427
plugins/mpv-bridge/android/mpv/MPVView.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
plugins/mpv-bridge/android/mpv/MpvPackage.kt
Normal file
16
plugins/mpv-bridge/android/mpv/MpvPackage.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
183
plugins/mpv-bridge/android/mpv/MpvPlayerViewManager.kt
Normal file
183
plugins/mpv-bridge/android/mpv/MpvPlayerViewManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
plugins/mpv-bridge/withMpvBridge.js
Normal file
94
plugins/mpv-bridge/withMpvBridge.js
Normal 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;
|
||||
Loading…
Reference in a new issue