mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
changed mpv hw to try "mediacode" first
This commit is contained in:
parent
7e7804b6d4
commit
0b4db84f30
3 changed files with 117 additions and 121 deletions
|
|
@ -93,95 +93,70 @@ class MPVView @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
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 configuration
|
||||
// 'mediacodec-copy' for hardware acceleration (GPU decoding, copies frames to CPU)
|
||||
// 'no' for software decoding (more compatible, especially on emulators)
|
||||
val hwdecValue = if (useHardwareDecoding) "mediacodec-copy" else "no"
|
||||
val hwdecValue = if (useHardwareDecoding) "mediacodec,mediacodec-copy" else "no"
|
||||
Log.d(TAG, "Hardware decoding: $useHardwareDecoding, hwdec value: $hwdecValue")
|
||||
MPVLib.setOptionString("hwdec", hwdecValue)
|
||||
MPVLib.setOptionString("hwdec-codecs", "all")
|
||||
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
||||
|
||||
MPVLib.setOptionString("target-colorspace-hint", "yes")
|
||||
MPVLib.setOptionString("vd-lavc-film-grain", "cpu")
|
||||
|
||||
// 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("demuxer-max-bytes", "67108864")
|
||||
MPVLib.setOptionString("demuxer-max-back-bytes", "33554432")
|
||||
MPVLib.setOptionString("cache", "yes")
|
||||
MPVLib.setOptionString("cache-secs", "30")
|
||||
|
||||
// Network options
|
||||
MPVLib.setOptionString("network-timeout", "60") // 60 second timeout
|
||||
|
||||
// CRITICAL: Disable youtube-dl/yt-dlp hook
|
||||
// The ytdl_hook incorrectly tries to parse HLS/direct URLs through youtube-dl
|
||||
// which fails on Android since yt-dlp is not available, causing playback failure
|
||||
MPVLib.setOptionString("network-timeout", "60")
|
||||
MPVLib.setOptionString("ytdl", "no")
|
||||
|
||||
// CRITICAL: HTTP headers MUST be set as options before init()
|
||||
// Apply headers if they were set before surface initialization
|
||||
applyHttpHeadersAsOptions()
|
||||
|
||||
// FFmpeg HTTP protocol options for better compatibility
|
||||
MPVLib.setOptionString("tls-verify", "no") // Disable TLS cert verification
|
||||
MPVLib.setOptionString("http-reconnect", "yes") // Auto-reconnect on network issues
|
||||
MPVLib.setOptionString("stream-reconnect", "yes") // Reconnect if stream drops
|
||||
MPVLib.setOptionString("tls-verify", "no")
|
||||
MPVLib.setOptionString("http-reconnect", "yes")
|
||||
MPVLib.setOptionString("stream-reconnect", "yes")
|
||||
|
||||
// CRITICAL: HLS demuxer options for proper VOD stream handling
|
||||
// Without these, HLS streams may be treated as live and start from the end
|
||||
// Note: Multiple lavf options separated by comma
|
||||
MPVLib.setOptionString("demuxer-lavf-o", "live_start_index=0,prefer_x_start=1,http_persistent=0")
|
||||
MPVLib.setOptionString("demuxer-seekable-cache", "yes") // Allow seeking in cached content
|
||||
MPVLib.setOptionString("force-seekable", "yes") // Force stream to be seekable
|
||||
MPVLib.setOptionString("demuxer-seekable-cache", "yes")
|
||||
MPVLib.setOptionString("force-seekable", "yes")
|
||||
|
||||
// Increase probe/analyze duration to help detect full HLS duration
|
||||
MPVLib.setOptionString("demuxer-lavf-probesize", "10000000") // 10MB probe size
|
||||
MPVLib.setOptionString("demuxer-lavf-analyzeduration", "10") // 10 seconds analyze
|
||||
MPVLib.setOptionString("demuxer-lavf-probesize", "10000000")
|
||||
MPVLib.setOptionString("demuxer-lavf-analyzeduration", "10")
|
||||
|
||||
// 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
|
||||
MPVLib.setOptionString("sub-auto", "fuzzy")
|
||||
MPVLib.setOptionString("sub-visibility", "yes")
|
||||
MPVLib.setOptionString("sub-font-size", "48")
|
||||
MPVLib.setOptionString("sub-pos", "95")
|
||||
MPVLib.setOptionString("sub-color", "#FFFFFFFF")
|
||||
MPVLib.setOptionString("sub-border-size", "3")
|
||||
MPVLib.setOptionString("sub-border-color", "#FF000000")
|
||||
MPVLib.setOptionString("sub-shadow-offset", "2")
|
||||
MPVLib.setOptionString("sub-shadow-color", "#80000000")
|
||||
|
||||
// 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("sub-font", "Roboto")
|
||||
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("sub-codepage", "auto")
|
||||
|
||||
MPVLib.setOptionString("osc", "no") // Disable on screen controller
|
||||
MPVLib.setOptionString("osc", "no")
|
||||
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
|
||||
MPVLib.setOptionString("sub-fix-timing", "yes")
|
||||
|
||||
// Force subtitle rendering
|
||||
MPVLib.setOptionString("sid", "auto") // Auto-select subtitle track
|
||||
MPVLib.setOptionString("sid", "auto")
|
||||
|
||||
// Disable terminal/input
|
||||
MPVLib.setOptionString("terminal", "no")
|
||||
MPVLib.setOptionString("input-default-bindings", "no")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const videoRef = useRef<any>(null);
|
||||
const mpvPlayerRef = useRef<MpvPlayerRef>(null);
|
||||
const pinchRef = useRef(null);
|
||||
const tracksHook = usePlayerTracks();
|
||||
|
||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
||||
|
|
@ -86,6 +87,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider);
|
||||
const [currentStreamName, setCurrentStreamName] = useState(streamName);
|
||||
|
||||
// State to force unmount VideoSurface during stream transitions
|
||||
const [isTransitioningStream, setIsTransitioningStream] = useState(false);
|
||||
|
||||
// Subtitle addon state
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false);
|
||||
|
|
@ -329,10 +333,14 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
modals.setShowSourcesModal(false);
|
||||
playerState.setPaused(true);
|
||||
|
||||
// Unmount VideoSurface first to ensure MPV is fully destroyed
|
||||
setIsTransitioningStream(true);
|
||||
|
||||
const newQuality = newStream.quality || newStream.title?.match(/(\d+)p/)?.[0];
|
||||
const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown';
|
||||
const newStreamName = newStream.name || newStream.title || 'Unknown';
|
||||
|
||||
// Wait for unmount to complete, then navigate
|
||||
setTimeout(() => {
|
||||
(navigation as any).replace('PlayerAndroid', {
|
||||
...route.params,
|
||||
|
|
@ -343,19 +351,24 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
headers: newStream.headers,
|
||||
availableStreams: availableStreams
|
||||
});
|
||||
}, 100);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleEpisodeStreamSelect = async (stream: any) => {
|
||||
if (!modals.selectedEpisodeForStreams) return;
|
||||
modals.setShowEpisodeStreamsModal(false);
|
||||
playerState.setPaused(true);
|
||||
|
||||
// Unmount VideoSurface first to ensure MPV is fully destroyed
|
||||
setIsTransitioningStream(true);
|
||||
|
||||
const ep = modals.selectedEpisodeForStreams;
|
||||
|
||||
const newQuality = stream.quality || (stream.title?.match(/(\d+)p/)?.[0]);
|
||||
const newProvider = stream.addonName || stream.name || stream.addon || 'Unknown';
|
||||
const newStreamName = stream.name || stream.title || 'Unknown Stream';
|
||||
|
||||
// Wait for unmount to complete, then navigate
|
||||
setTimeout(() => {
|
||||
(navigation as any).replace('PlayerAndroid', {
|
||||
uri: stream.url,
|
||||
|
|
@ -376,7 +389,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
availableStreams: {},
|
||||
groupedEpisodes: groupedEpisodes,
|
||||
});
|
||||
}, 100);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// Subtitle addon fetching
|
||||
|
|
@ -489,73 +502,75 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
/>
|
||||
|
||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||
<VideoSurface
|
||||
processedStreamUrl={currentStreamUrl}
|
||||
headers={headers}
|
||||
volume={volume}
|
||||
playbackSpeed={speedControl.playbackSpeed}
|
||||
resizeMode={playerState.resizeMode}
|
||||
paused={playerState.paused}
|
||||
currentStreamUrl={currentStreamUrl}
|
||||
toggleControls={toggleControls}
|
||||
onLoad={handleLoad}
|
||||
onProgress={handleProgress}
|
||||
onSeek={(data) => {
|
||||
playerState.isSeeking.current = false;
|
||||
if (data.currentTime) traktAutosync.handleProgressUpdate(data.currentTime, playerState.duration, true);
|
||||
}}
|
||||
onEnd={() => {
|
||||
if (modals.showEpisodeStreamsModal) return;
|
||||
playerState.setPaused(true);
|
||||
}}
|
||||
onError={(err: any) => {
|
||||
logger.error('Video Error', err);
|
||||
{!isTransitioningStream && (
|
||||
<VideoSurface
|
||||
processedStreamUrl={currentStreamUrl}
|
||||
headers={headers}
|
||||
volume={volume}
|
||||
playbackSpeed={speedControl.playbackSpeed}
|
||||
resizeMode={playerState.resizeMode}
|
||||
paused={playerState.paused}
|
||||
currentStreamUrl={currentStreamUrl}
|
||||
toggleControls={toggleControls}
|
||||
onLoad={handleLoad}
|
||||
onProgress={handleProgress}
|
||||
onSeek={(data) => {
|
||||
playerState.isSeeking.current = false;
|
||||
if (data.currentTime) traktAutosync.handleProgressUpdate(data.currentTime, playerState.duration, true);
|
||||
}}
|
||||
onEnd={() => {
|
||||
if (modals.showEpisodeStreamsModal) return;
|
||||
playerState.setPaused(true);
|
||||
}}
|
||||
onError={(err: any) => {
|
||||
logger.error('Video Error', err);
|
||||
|
||||
// Determine the actual error message
|
||||
let displayError = 'An unknown error occurred';
|
||||
// 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);
|
||||
}
|
||||
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)}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
mpvPlayerRef={mpvPlayerRef}
|
||||
pinchRef={useRef(null)}
|
||||
onPinchGestureEvent={() => { }}
|
||||
onPinchHandlerStateChange={() => { }}
|
||||
screenDimensions={playerState.screenDimensions}
|
||||
useHardwareDecoding={settings.useHardwareDecoding}
|
||||
/>
|
||||
modals.setErrorDetails(displayError);
|
||||
modals.setShowErrorModal(true);
|
||||
}}
|
||||
onBuffer={(buf) => playerState.setIsBuffering(buf.isBuffering)}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
mpvPlayerRef={mpvPlayerRef}
|
||||
pinchRef={pinchRef}
|
||||
onPinchGestureEvent={() => { }}
|
||||
onPinchHandlerStateChange={() => { }}
|
||||
screenDimensions={playerState.screenDimensions}
|
||||
useHardwareDecoding={settings.useHardwareDecoding}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Custom Subtitles for addon subtitles */}
|
||||
<CustomSubtitles
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Animated, {
|
|||
Easing,
|
||||
SharedValue,
|
||||
} from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { parentalGuideService } from '../../../services/parentalGuideService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
|
|
@ -66,6 +67,7 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
|||
shouldShow,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
const [warnings, setWarnings] = useState<WarningItem[]>([]);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
|
@ -231,8 +233,12 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
|||
const lineWidth = Math.min(3, screenWidth * 0.0038);
|
||||
const containerPadding = Math.min(20, screenWidth * 0.025);
|
||||
|
||||
// Use left inset for landscape notches, top inset for portrait
|
||||
const safeLeftOffset = insets.left + containerPadding;
|
||||
const safeTopOffset = containerPadding;
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, { left: containerPadding, top: containerPadding + 30 }]} pointerEvents="none">
|
||||
<Animated.View style={[styles.container, { left: safeLeftOffset, top: safeTopOffset }]} pointerEvents="none">
|
||||
{/* Vertical line - animates height */}
|
||||
<Animated.View style={[styles.line, lineStyle, { backgroundColor: currentTheme.colors.primary, width: lineWidth }]} />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue