mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-05 01:09:46 +00:00
fixed subtitle rendering, added aspect ratio support
This commit is contained in:
parent
9504d48607
commit
3cea291901
5 changed files with 256 additions and 7 deletions
|
|
@ -28,6 +28,7 @@ class MPVView @JvmOverloads constructor(
|
|||
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
|
||||
|
|
@ -89,8 +90,13 @@ class MPVView @JvmOverloads constructor(
|
|||
MPVLib.setOptionString("vo", "gpu")
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
MPVLib.setOptionString("opengl-es", "yes")
|
||||
MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy")
|
||||
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
||||
|
||||
// Hardware decoding - use mediacodec-copy to allow subtitle overlay
|
||||
// 'mediacodec-copy' copies frames to CPU memory which enables subtitle blending
|
||||
MPVLib.setOptionString("hwdec", "mediacodec-copy")
|
||||
MPVLib.setOptionString("hwdec-codecs", "all")
|
||||
|
||||
// Audio output
|
||||
MPVLib.setOptionString("ao", "audiotrack,opensles")
|
||||
|
||||
// Network caching for streaming
|
||||
|
|
@ -99,6 +105,43 @@ class MPVView @JvmOverloads constructor(
|
|||
MPVLib.setOptionString("cache", "yes")
|
||||
MPVLib.setOptionString("cache-secs", "30")
|
||||
|
||||
// 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")
|
||||
|
|
@ -120,6 +163,11 @@ class MPVView @JvmOverloads constructor(
|
|||
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) {
|
||||
|
|
@ -176,11 +224,52 @@ class MPVView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,10 +280,58 @@ class MPVView @JvmOverloads constructor(
|
|||
Log.d(TAG, "Property changed: $property")
|
||||
when (property) {
|
||||
"track-list" -> {
|
||||
// Track list updated, could notify JS about available tracks
|
||||
// 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)")
|
||||
|
|
@ -244,7 +381,22 @@ class MPVView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
MPV_EVENT_END_FILE -> {
|
||||
onEndCallback?.invoke()
|
||||
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") ?: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,35 @@ class MpvPlayerViewManager(
|
|||
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
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +101,7 @@ class MpvPlayerViewManager(
|
|||
.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()
|
||||
}
|
||||
|
||||
|
|
@ -128,4 +158,9 @@ class MpvPlayerViewManager(
|
|||
// 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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -396,11 +396,45 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
modals.setErrorDetails(JSON.stringify(err));
|
||||
// Determine the actual error message
|
||||
let displayError = 'An unknown error occurred';
|
||||
|
||||
if (typeof err?.error === 'string') {
|
||||
displayError = err.error;
|
||||
} else if (err?.error?.errorString) {
|
||||
displayError = err.error.errorString;
|
||||
} else if (err?.errorString) {
|
||||
displayError = err.errorString;
|
||||
} else if (typeof err === 'string') {
|
||||
displayError = err;
|
||||
} else {
|
||||
displayError = JSON.stringify(err);
|
||||
}
|
||||
|
||||
modals.setErrorDetails(displayError);
|
||||
modals.setShowErrorModal(true);
|
||||
}}
|
||||
onBuffer={(buf) => playerState.setIsBuffering(buf.isBuffering)}
|
||||
onTracksUpdate={vlcHook.handleVlcTracksUpdate}
|
||||
onTracksChanged={(data) => {
|
||||
console.log('[AndroidVideoPlayer] onTracksChanged:', data);
|
||||
if (data?.audioTracks) {
|
||||
const formatted = data.audioTracks.map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name || `Track ${t.id}`,
|
||||
language: t.language
|
||||
}));
|
||||
tracksHook.setRnVideoAudioTracks(formatted);
|
||||
}
|
||||
if (data?.subtitleTracks) {
|
||||
const formatted = data.subtitleTracks.map((t: any) => ({
|
||||
id: t.id,
|
||||
name: t.name || `Track ${t.id}`,
|
||||
language: t.language
|
||||
}));
|
||||
tracksHook.setRnVideoTextTracks(formatted);
|
||||
}
|
||||
}}
|
||||
vlcPlayerRef={vlcHook.vlcPlayerRef}
|
||||
mpvPlayerRef={mpvPlayerRef}
|
||||
videoRef={videoRef}
|
||||
|
|
@ -504,8 +538,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
ksAudioTracks={tracksHook.ksAudioTracks}
|
||||
selectedAudioTrack={tracksHook.computedSelectedAudioTrack}
|
||||
selectAudioTrack={(trackId) => {
|
||||
useVLC ? vlcHook.selectVlcAudioTrack(trackId) :
|
||||
if (useVLC) {
|
||||
vlcHook.selectVlcAudioTrack(trackId);
|
||||
} else {
|
||||
tracksHook.setSelectedAudioTrack(trackId === null ? null : { type: 'index', value: trackId });
|
||||
// Actually tell MPV to switch the audio track
|
||||
if (trackId !== null && mpvPlayerRef.current) {
|
||||
mpvPlayerRef.current.setAudioTrack(trackId);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
@ -527,7 +568,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
fetchAvailableSubtitles={() => { }} // Placeholder
|
||||
loadWyzieSubtitle={() => { }} // Placeholder
|
||||
selectTextTrack={(trackId) => {
|
||||
useVLC ? vlcHook.selectVlcSubtitleTrack(trackId) : tracksHook.setSelectedTextTrack(trackId);
|
||||
if (useVLC) {
|
||||
vlcHook.selectVlcSubtitleTrack(trackId);
|
||||
} else {
|
||||
tracksHook.setSelectedTextTrack(trackId);
|
||||
// Actually tell MPV to switch the subtitle track
|
||||
if (mpvPlayerRef.current) {
|
||||
mpvPlayerRef.current.setSubtitleTrack(trackId);
|
||||
}
|
||||
}
|
||||
modals.setShowSubtitleModal(false);
|
||||
}}
|
||||
disableCustomSubtitles={() => { }} // Placeholder
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ export interface MpvPlayerProps {
|
|||
paused?: boolean;
|
||||
volume?: number;
|
||||
rate?: number;
|
||||
resizeMode?: 'contain' | 'cover' | 'stretch';
|
||||
style?: any;
|
||||
onLoad?: (data: { duration: number; width: number; height: number }) => void;
|
||||
onProgress?: (data: { currentTime: number; duration: number }) => void;
|
||||
onEnd?: () => void;
|
||||
onError?: (error: { error: string }) => void;
|
||||
onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void;
|
||||
}
|
||||
|
||||
const MpvPlayer = forwardRef<MpvPlayerRef, MpvPlayerProps>((props, ref) => {
|
||||
|
|
@ -80,6 +82,11 @@ const MpvPlayer = forwardRef<MpvPlayerRef, MpvPlayerProps>((props, ref) => {
|
|||
props.onError?.(event?.nativeEvent);
|
||||
};
|
||||
|
||||
const handleTracksChanged = (event: any) => {
|
||||
console.log('[MpvPlayer] Native onTracksChanged event:', event?.nativeEvent);
|
||||
props.onTracksChanged?.(event?.nativeEvent);
|
||||
};
|
||||
|
||||
return (
|
||||
<MpvPlayerNative
|
||||
ref={nativeRef}
|
||||
|
|
@ -88,10 +95,12 @@ const MpvPlayer = forwardRef<MpvPlayerRef, MpvPlayerProps>((props, ref) => {
|
|||
paused={props.paused ?? true}
|
||||
volume={props.volume ?? 1.0}
|
||||
rate={props.rate ?? 1.0}
|
||||
resizeMode={props.resizeMode ?? 'contain'}
|
||||
onLoad={handleLoad}
|
||||
onProgress={handleProgress}
|
||||
onEnd={handleEnd}
|
||||
onError={handleError}
|
||||
onTracksChanged={handleTracksChanged}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ interface VideoSurfaceProps {
|
|||
loadStartAtRef?: any;
|
||||
firstFrameAtRef?: any;
|
||||
zoomScale?: number;
|
||||
onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void;
|
||||
}
|
||||
|
||||
export const VideoSurface: React.FC<VideoSurfaceProps> = ({
|
||||
|
|
@ -72,6 +73,7 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
|
|||
onPinchGestureEvent,
|
||||
onPinchHandlerStateChange,
|
||||
screenDimensions,
|
||||
onTracksChanged,
|
||||
}) => {
|
||||
// Use the actual stream URL
|
||||
const streamUrl = currentStreamUrl || processedStreamUrl;
|
||||
|
|
@ -122,11 +124,13 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
|
|||
paused={paused}
|
||||
volume={volume}
|
||||
rate={playbackSpeed}
|
||||
resizeMode={resizeMode === 'none' ? 'contain' : resizeMode}
|
||||
style={localStyles.player}
|
||||
onLoad={handleLoad}
|
||||
onProgress={handleProgress}
|
||||
onEnd={handleEnd}
|
||||
onError={handleError}
|
||||
onTracksChanged={onTracksChanged}
|
||||
/>
|
||||
|
||||
{/* Gesture overlay - transparent, on top of the player */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue