madari-oss/lib/features/doc_viewer/container/video_viewer.dart
omkar 37383a5b41
Some checks failed
Build and Deploy / build_windows (push) Has been cancelled
Build and Deploy / build_android (push) Has been cancelled
Build and Deploy / build_android_tv (push) Has been cancelled
Build and Deploy / build_ipa (push) Has been cancelled
Build and Deploy / build_linux (push) Has been cancelled
Build and Deploy / build_macos (push) Has been cancelled
fix: support for disable hardware acceleration
2025-01-12 19:57:02 +05:30

349 lines
8.7 KiB
Dart

import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/connections/service/base_connection_service.dart';
import 'package:madari_client/features/watch_history/service/base_watch_history.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import '../../../utils/load_language.dart';
import '../../connections/types/stremio/stremio_base.types.dart' as types;
import '../../connections/widget/stremio/stremio_season_selector.dart';
import '../../trakt/service/trakt.service.dart';
import '../../trakt/types/common.dart';
import '../../watch_history/service/zeee_watch_history.dart';
import '../types/doc_source.dart';
import 'video_viewer/video_viewer_ui.dart';
class VideoViewer extends StatefulWidget {
final DocSource source;
final LibraryItem? meta;
final BaseConnectionService? service;
final String? currentSeason;
final String? library;
const VideoViewer({
super.key,
required this.source,
this.meta,
this.service,
this.currentSeason,
this.library,
});
@override
State<VideoViewer> createState() => _VideoViewerState();
}
class _VideoViewerState extends State<VideoViewer> {
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
Timer? _timer;
late final Player player = Player(
configuration: const PlayerConfiguration(
title: "Madari",
),
);
final Logger _logger = Logger('VideoPlayer');
double get currentProgressInPercentage {
final duration = player.state.duration.inSeconds;
final position = player.state.position.inSeconds;
return duration > 0 ? (position / duration * 100) : 0;
}
Future<List<TraktProgress>>? traktProgress;
Future<void> saveWatchHistory() async {
final duration = player.state.duration.inSeconds;
if (duration <= 30) {
_logger.info('Video is too short to track.');
return;
}
final position = player.state.position.inSeconds;
final progress = duration > 0 ? (position / duration * 100).round() : 0;
if (progress == 0) {
_logger.info('No progress to save.');
return;
}
if (widget.meta is types.Meta && TraktService.instance != null) {
try {
if (player.state.playing) {
_logger.info('Starting scrobbling...');
await TraktService.instance!.startScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
);
} else {
_logger.info('Stopping scrobbling...');
await TraktService.instance!.stopScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
);
}
} catch (e) {
_logger.severe('Error during scrobbling: $e');
TraktService.instance!.debugLogs.add(e.toString());
}
} else {
_logger.warning('Meta is not valid or TraktService is not initialized.');
}
await zeeeWatchHistory!.saveWatchHistory(
history: WatchHistory(
id: _source.id,
progress: progress,
duration: duration.toDouble(),
episode: _source.episode,
season: _source.season,
),
);
}
late final controller = VideoController(
player,
configuration: VideoControllerConfiguration(
enableHardwareAcceleration: !config.softwareAcceleration,
),
);
late DocSource _source;
bool canCallOnce = false;
int? traktId;
Future<void> setDurationFromTrakt() async {
if (player.state.duration.inSeconds < 2) {
return;
}
if (canCallOnce) {
return;
}
canCallOnce = true;
if (!TraktService.isEnabled() || traktProgress == null) {
player.play();
return;
}
final progress = await traktProgress;
if ((progress ?? []).isEmpty) {
player.play();
return;
}
traktId = progress!.first.traktId;
final duration = Duration(
seconds: calculateSecondsFromProgress(
player.state.duration.inSeconds.toDouble(),
progress.first.progress,
),
);
await player.seek(duration);
await player.play();
}
List<StreamSubscription> listener = [];
PlaybackConfig config = getPlaybackConfig();
@override
void initState() {
super.initState();
_source = widget.source;
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
);
_duration = player.stream.duration.listen((item) async {
if (item.inSeconds != 0) {
await setDurationFromTrakt();
await saveWatchHistory();
}
});
loadFile();
if (player.platform is NativePlayer && !kIsWeb) {
Future.microtask(() async {
await (player.platform as dynamic).setProperty('network-timeout', '60');
});
}
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
saveWatchHistory();
});
_streamListen = player.stream.playing.listen((playing) {
if (playing) {
saveWatchHistory();
}
});
if (widget.meta is types.Meta && TraktService.isEnabled()) {
traktProgress = TraktService.instance!.getProgress(
widget.meta as types.Meta,
);
}
}
loadFile() async {
_logger.info('Loading file for source: ${_source.id}');
final item = await zeeeWatchHistory!.getItemWatchHistory(
ids: [
WatchHistoryGetRequest(
id: _source.id,
season: _source.season,
episode: _source.episode,
),
],
);
final duration = Duration(
seconds: item.isEmpty
? 0
: calculateSecondsFromProgress(
item.first.duration,
item.first.progress.toDouble(),
),
);
switch (_source.runtimeType) {
case const (FileSource):
if (kIsWeb) {
return;
}
player.open(
Media(
(_source as FileSource).filePath,
start: duration,
),
play: false,
);
case const (URLSource):
case const (MediaURLSource):
case const (TorrentSource):
player.open(
Media(
(_source as URLSource).url,
httpHeaders: (_source as URLSource).headers,
start: duration,
),
play: false,
);
}
}
late StreamSubscription<bool> _streamComplete;
late StreamSubscription<bool> _streamListen;
late StreamSubscription<dynamic> _duration;
onLibrarySelect() async {
_logger.info('Library selection triggered.');
controller.player.pause();
final result = await showCupertinoDialog(
context: context,
builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text("Seasons"),
),
body: CustomScrollView(
slivers: [
StremioItemSeasonSelector(
service: widget.service,
meta: widget.meta as types.Meta,
shouldPop: true,
season: int.tryParse(widget.currentSeason!),
),
],
),
);
},
);
if (result is MediaURLSource) {
_source = result;
loadFile();
}
}
@override
void dispose() {
_logger.info('Disposing VideoViewer...');
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
for (final item in listener) {
item.cancel();
}
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
overlays: [],
);
_timer?.cancel();
_streamListen.cancel();
_duration.cancel();
if (widget.meta is types.Meta && player.state.duration.inSeconds > 30) {
TraktService.instance!.stopScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
shouldClearCache: true,
traktId: traktId,
);
}
player.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: VideoViewerUi(
controller: controller,
player: player,
config: config,
source: _source,
onLibrarySelect: onLibrarySelect,
title: _source.title,
service: widget.service,
meta: widget.meta,
),
);
}
}
int calculateSecondsFromProgress(
double duration,
double progressPercentage,
) {
final clampedProgress = progressPercentage.clamp(0.0, 100.0);
final currentSeconds = (duration * (clampedProgress / 100)).round();
return currentSeconds;
}