Compare commits

..
2 Commits
Author SHA1 Message Date
RarDog c7f507836b fix: fullscreen playback state retention and responsive control buttons (v3.1.8)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:33:42 +03:00
RarDog 1395cfd414 fix: remove unsafe libmpv setProperty FFI calls causing SIGSEGV crash (v3.1.7)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:17:15 +03:00
6 changed files with 132 additions and 80 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.6-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.6" /></a> <a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.8-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.8" /></a>
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" /> <img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-00C853?style=for-the-badge" alt="Platforms" /> <img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-00C853?style=for-the-badge" alt="Platforms" />
<a href="https://github.com/RarDog/Lunaris/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a> <a href="https://github.com/RarDog/Lunaris/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.1.6'; const appDisplayVersion = '3.1.8';
const appBuildNumber = 67; const appBuildNumber = 69;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+19
View File
@@ -1,4 +1,23 @@
const lunarisChangelog = [ const lunarisChangelog = [
LunarisChange(
version: '3.1.8',
title: 'Безупречный полноэкранный плеер: устранение остановки при развертывании и отзывчивые кнопки управления',
bullets: [
'Непрерывное воспроизведение при открытии на весь экран: видео больше не останавливается самопроизвольно при входе в полноэкранный режим или смене ориентации устройства.',
'Корректный статус воспроизведения: устранен сброс кнопки на «Пуск» при переходе в полный экран — видео продолжает воспроизведение без ложных остановок.',
'Мгновенный отклик кнопок «Пуск/Пауза» и перемотки: жесты управления поверхностью (двойной тап, смахивание громкости) перемещены в фоновый слой, исключая перехват и отмену нажатий по кнопкам управления.',
'Синхронизация состояния при сворачивании: плеер точно запоминает состояние паузы/воспроизведения при выходе из полноэкранного режима обратно в пост.',
],
),
LunarisChange(
version: '3.1.7',
title: 'Устранение критического сбоя при открытии видеопоста (SIGSEGV в libmpv)',
bullets: [
'Устранение нативного вылета: полностью удалены небезопасные прямые FFI-вызовы setProperty к mpv, приводившие к немедленному падению приложения на Android при инициализации видео.',
'Безопасная конфигурация декодирования: перевод настроек аппаратного ускорения и фоллбэка кодеков на официальный типобезопасный VideoControllerConfiguration (media_kit_video).',
'Надежный фоллбэк при редких ошибках кодека: при несовместимости аппаратного декодера плеер бесшовно переключается на программный FFmpeg-декодер без риска краша.',
],
),
LunarisChange( LunarisChange(
version: '3.1.6', version: '3.1.6',
title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека', title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека',
@@ -75,6 +75,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
late double _currentVolume; late double _currentVolume;
String? _videoError; String? _videoError;
bool _retriedFormatError = false; bool _retriedFormatError = false;
bool _softwareDecodingFallback = false;
Timer? _hideTimer; Timer? _hideTimer;
StreamSubscription<String>? _errorSubscription; StreamSubscription<String>? _errorSubscription;
StreamSubscription<Duration>? _positionSubscription; StreamSubscription<Duration>? _positionSubscription;
@@ -105,6 +106,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
_videoIndex = 0; _videoIndex = 0;
_videoError = null; _videoError = null;
_retriedFormatError = false; _retriedFormatError = false;
_softwareDecodingFallback = false;
_imageUrls = _buildImageUrls(widget.post); _imageUrls = _buildImageUrls(widget.post);
_videoUrls = _buildVideoUrls(widget.post); _videoUrls = _buildVideoUrls(widget.post);
if (_isVideo(widget.post) && _videoUrls.isNotEmpty) { if (_isVideo(widget.post) && _videoUrls.isNotEmpty) {
@@ -359,13 +361,13 @@ class _PostMediaViewerState extends State<PostMediaViewer>
void _initializeVideo() { void _initializeVideo() {
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
_player = Player(); _player = Player();
_controller = VideoController(_player!); _controller = VideoController(
try { _player!,
final native = _player!.platform as dynamic; configuration: VideoControllerConfiguration(
native?.setProperty('hwdec', 'auto-safe', waitForInitialization: false); enableHardwareAcceleration: !_softwareDecodingFallback,
native?.setProperty('vd-lavc-software-fallback', 'yes', waitForInitialization: false); hwdec: _softwareDecodingFallback ? 'no' : 'auto-safe',
native?.setProperty('vd-lavc-check-hw-profile', 'yes', waitForInitialization: false); ),
} catch (_) {} );
_applyVolume(); _applyVolume();
_player!.setPlaylistMode( _player!.setPlaylistMode(
_loopVideo ? PlaylistMode.single : PlaylistMode.none, _loopVideo ? PlaylistMode.single : PlaylistMode.none,
@@ -437,14 +439,12 @@ class _PostMediaViewerState extends State<PostMediaViewer>
lower.contains('stream format not recognized') || lower.contains('stream format not recognized') ||
lower.contains('failed to recognize file format'))) { lower.contains('failed to recognize file format'))) {
_retriedFormatError = true; _retriedFormatError = true;
try { _softwareDecodingFallback = true;
final native = _player?.platform as dynamic; _disposeVideo();
await native?.setProperty('hwdec', 'no', waitForInitialization: false); await Future<void>.delayed(const Duration(milliseconds: 250));
} catch (_) {} if (mounted) {
await Future<void>.delayed(const Duration(milliseconds: 300));
if (mounted && _player != null) {
_videoIndex = 0; _videoIndex = 0;
await _openVideo(play: play); _initializeVideo();
return; return;
} }
} }
@@ -456,11 +456,12 @@ class _PostMediaViewerState extends State<PostMediaViewer>
Future<void> _retryVideo() async { Future<void> _retryVideo() async {
_videoIndex = 0; _videoIndex = 0;
_retriedFormatError = false; _retriedFormatError = false;
try { _softwareDecodingFallback = true;
final native = _player?.platform as dynamic; _disposeVideo();
await native?.setProperty('hwdec', 'no', waitForInitialization: false); await Future<void>.delayed(const Duration(milliseconds: 150));
} catch (_) {} if (mounted) {
await _openVideo(play: true); _initializeVideo();
}
} }
void _disposeVideo() { void _disposeVideo() {
@@ -570,6 +571,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
final player = _player; final player = _player;
final controller = _controller; final controller = _controller;
if (player == null || controller == null) return; if (player == null || controller == null) return;
final wasPlaying = player.state.playing;
setState(() => _inFullscreen = true); setState(() => _inFullscreen = true);
if (!context.mounted) return; if (!context.mounted) return;
final result = await Navigator.of(context, rootNavigator: true) final result = await Navigator.of(context, rootNavigator: true)
@@ -579,6 +581,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
pageBuilder: (_, __, ___) => _FullscreenVideoPage( pageBuilder: (_, __, ___) => _FullscreenVideoPage(
player: player, player: player,
controller: controller, controller: controller,
wasPlaying: wasPlaying,
aspectRatio: widget.post.width > 0 && widget.post.height > 0 aspectRatio: widget.post.width > 0 && widget.post.height > 0
? widget.post.width / widget.post.height ? widget.post.width / widget.post.height
: 16 / 9, : 16 / 9,
@@ -602,6 +605,11 @@ class _PostMediaViewerState extends State<PostMediaViewer>
if (mounted) setState(() => _inFullscreen = false); if (mounted) setState(() => _inFullscreen = false);
if (result != null) { if (result != null) {
await _applyPlaybackSnapshot(result); await _applyPlaybackSnapshot(result);
if (result.playing && !player.state.playing) {
await player.play();
} else if (!result.playing && player.state.playing) {
await player.pause();
}
return; return;
} }
} }
@@ -728,6 +736,7 @@ class _FullscreenVideoPage extends StatefulWidget {
const _FullscreenVideoPage({ const _FullscreenVideoPage({
required this.player, required this.player,
required this.controller, required this.controller,
required this.wasPlaying,
required this.aspectRatio, required this.aspectRatio,
required this.loopVideo, required this.loopVideo,
required this.muted, required this.muted,
@@ -741,6 +750,7 @@ class _FullscreenVideoPage extends StatefulWidget {
final Player player; final Player player;
final VideoController controller; final VideoController controller;
final bool wasPlaying;
final double aspectRatio; final double aspectRatio;
final bool loopVideo; final bool loopVideo;
final bool muted; final bool muted;
@@ -778,6 +788,13 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
_applyOrientation(); _applyOrientation();
_scheduleControlsHide(); _scheduleControlsHide();
if (widget.wasPlaying) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !widget.player.state.playing) {
widget.player.play();
}
});
}
} }
void _applyOrientation() { void _applyOrientation() {
@@ -1130,6 +1147,7 @@ class _VideoSurfaceState extends State<_VideoSurface> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final player = widget.player; final player = widget.player;
Offset? lastTapDown;
final child = LayoutBuilder( final child = LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -1142,6 +1160,32 @@ class _VideoSurfaceState extends State<_VideoSurface> {
controller: widget.controller, controller: widget.controller,
fit: widget.coverVideo ? BoxFit.cover : BoxFit.contain, fit: widget.coverVideo ? BoxFit.cover : BoxFit.contain,
controls: null, controls: null,
pauseUponEnteringBackgroundMode: false,
resumeUponEnteringForegroundMode: false,
),
),
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => lastTapDown = details.localPosition,
onTap: widget.onInteract,
onDoubleTap: () {
if (lastTapDown != null) {
_onDoubleTapAt(
lastTapDown!,
MediaQuery.sizeOf(context).width,
);
}
},
onLongPressStart: (_) => _startSpeedBoost(),
onLongPressEnd: (_) => _stopSpeedBoost(),
onVerticalDragUpdate: (details) {
final x = details.localPosition.dx;
final width = MediaQuery.sizeOf(context).width;
if (x > width * 0.4) {
_adjustVolume(details.primaryDelta ?? 0.0);
}
},
), ),
), ),
StreamBuilder<bool>( StreamBuilder<bool>(
@@ -1198,36 +1242,16 @@ class _VideoSurfaceState extends State<_VideoSurface> {
}, },
); );
Offset? lastTapDown;
return MouseRegion( return MouseRegion(
onHover: (_) => widget.onInteract(), onHover: (_) => widget.onInteract(),
child: GestureDetector( child: ClipRRect(
onTapDown: (details) => lastTapDown = details.localPosition, borderRadius: BorderRadius.circular(widget.fullscreen ? 0 : 10),
onTap: widget.onInteract, child: widget.fullscreen
onDoubleTap: () { ? SizedBox.expand(child: child)
if (lastTapDown != null) { : AspectRatio(
_onDoubleTapAt(lastTapDown!, MediaQuery.sizeOf(context).width); aspectRatio: widget.aspectRatio.clamp(0.35, 2.4),
} child: child,
}, ),
onLongPressStart: (_) => _startSpeedBoost(),
onLongPressEnd: (_) => _stopSpeedBoost(),
onVerticalDragUpdate: (details) {
final x = details.localPosition.dx;
final width = MediaQuery.sizeOf(context).width;
if (x > width * 0.4) {
_adjustVolume(details.primaryDelta ?? 0.0);
}
},
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.fullscreen ? 0 : 10),
child: widget.fullscreen
? SizedBox.expand(child: child)
: AspectRatio(
aspectRatio: widget.aspectRatio.clamp(0.35, 2.4),
child: child,
),
),
), ),
); );
} }
@@ -1545,32 +1569,37 @@ class _RoundControlButton extends StatelessWidget {
: Colors.white; : Colors.white;
return Tooltip( return Tooltip(
message: tooltip, message: tooltip,
child: GestureDetector( child: Material(
behavior: HitTestBehavior.opaque, color: Colors.transparent,
onTap: onPressed, shape: const CircleBorder(),
child: AnimatedContainer( child: InkResponse(
duration: AppMotion.duration(context, 140), containedInkWell: true,
width: size, highlightShape: BoxShape.circle,
height: size, onTap: onPressed,
decoration: BoxDecoration( child: AnimatedContainer(
color: background, duration: AppMotion.duration(context, 140),
shape: BoxShape.circle, width: size,
border: Border.all( height: size,
color: selected decoration: BoxDecoration(
? scheme.primary.withValues(alpha: 0.4) color: background,
: Colors.white.withValues(alpha: 0.14), shape: BoxShape.circle,
border: Border.all(
color: selected
? scheme.primary.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.14),
),
boxShadow: [
if (emphasized)
BoxShadow(
color: scheme.primary.withValues(alpha: 0.45),
blurRadius: 28,
spreadRadius: 2,
),
],
),
child: Center(
child: Icon(icon, color: foreground, size: iconSize),
), ),
boxShadow: [
if (emphasized)
BoxShadow(
color: scheme.primary.withValues(alpha: 0.45),
blurRadius: 28,
spreadRadius: 2,
),
],
),
child: Center(
child: Icon(icon, color: foreground, size: iconSize),
), ),
), ),
), ),
@@ -1981,11 +2010,13 @@ class _VideoControls extends StatelessWidget {
emphasized: true, emphasized: true,
onPressed: () async { onPressed: () async {
try { try {
if (player.state.completed) { if (player.state.playing) {
await player.seek(Duration.zero); await player.pause();
await player.play();
} else { } else {
await player.playOrPause(); if (player.state.completed) {
await player.seek(Duration.zero);
}
await player.play();
} }
} catch (_) { } catch (_) {
if (player.state.playing) { if (player.state.playing) {
+2
View File
@@ -596,6 +596,8 @@ class _FeedVideoPreviewState extends State<_FeedVideoPreview> {
controller: _controller, controller: _controller,
fit: BoxFit.cover, fit: BoxFit.cover,
controls: null, controls: null,
pauseUponEnteringBackgroundMode: false,
resumeUponEnteringForegroundMode: false,
), ),
), ),
), ),
+1 -1
View File
@@ -1,7 +1,7 @@
name: gel_rule_app name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app. description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none" publish_to: "none"
version: 3.1.6+67 version: 3.1.8+69
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"