Spaces:
Paused
Paused
| // lib/widgets/video_player/buffer_manager.dart | |
| import 'dart:async'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:tikslop/config/config.dart'; | |
| import 'package:tikslop/services/clip_queue/video_clip.dart'; | |
| import 'package:tikslop/services/clip_queue/clip_queue_manager.dart'; | |
| import 'package:video_player/video_player.dart'; | |
| import 'package:tikslop/models/video_result.dart'; | |
| import 'package:tikslop/models/video_orientation.dart'; | |
| /// Manages buffering and clip preloading for video player | |
| class BufferManager { | |
| /// The queue manager for clips | |
| final ClipQueueManager queueManager; | |
| /// Whether the manager is disposed | |
| bool isDisposed = false; | |
| /// Loading progress (0.0 to 1.0) | |
| double loadingProgress = 0.0; | |
| /// Timer for showing loading progress | |
| Timer? progressTimer; | |
| /// Timer for debug printing | |
| Timer? debugTimer; | |
| /// The video result | |
| final VideoResult video; | |
| /// Callback when queue is updated | |
| final Function() onQueueUpdated; | |
| /// Constructor | |
| BufferManager({ | |
| required this.video, | |
| required this.onQueueUpdated, | |
| ClipQueueManager? existingQueueManager, | |
| }) : queueManager = existingQueueManager ?? ClipQueueManager( | |
| video: video, | |
| onQueueUpdated: onQueueUpdated, | |
| ); | |
| /// Initialize the buffer with clips | |
| Future<void> initialize() async { | |
| if (isDisposed) return; | |
| // Start loading progress animation | |
| startLoadingProgress(); | |
| // Initialize queue manager but don't await it | |
| await queueManager.initialize(); | |
| } | |
| /// Start loading progress animation | |
| void startLoadingProgress() { | |
| progressTimer?.cancel(); | |
| loadingProgress = 0.0; | |
| const totalDuration = Duration(seconds: 12); | |
| const updateInterval = Duration(milliseconds: 50); | |
| final steps = totalDuration.inMilliseconds / updateInterval.inMilliseconds; | |
| final increment = 1.0 / steps; | |
| progressTimer = Timer.periodic(updateInterval, (timer) { | |
| if (isDisposed) { | |
| timer.cancel(); | |
| return; | |
| } | |
| loadingProgress += increment; | |
| if (loadingProgress >= 1.0) { | |
| progressTimer?.cancel(); | |
| } | |
| }); | |
| } | |
| /// Start debug printing (for development) | |
| void startDebugPrinting() { | |
| debugTimer = Timer.periodic(const Duration(seconds: 5), (_) { | |
| if (!isDisposed) { | |
| queueManager.printQueueState(); | |
| } | |
| }); | |
| } | |
| /// Check if buffer is ready to start playback | |
| bool isBufferReadyToStartPlayback() { | |
| final readyClips = queueManager.clipBuffer.where((c) => c.isReady).length; | |
| final totalClips = queueManager.clipBuffer.length; | |
| final bufferPercentage = (readyClips / totalClips * 100); | |
| return bufferPercentage >= Configuration.instance.minimumBufferPercentToStartPlayback; | |
| } | |
| /// Ensure the buffer remains full | |
| void ensureBufferFull() { | |
| if (isDisposed) return; | |
| // Add additional safety check to avoid errors if the widget has been disposed | |
| // but this method is still being called by an ongoing operation | |
| try { | |
| queueManager.fillBuffer(); | |
| } catch (e) { | |
| debugPrint('Error filling buffer: $e'); | |
| } | |
| } | |
| /// Preload the next clip to ensure smooth playback | |
| Future<VideoPlayerController?> preloadNextClip() async { | |
| if (isDisposed) return null; | |
| VideoPlayerController? nextController; | |
| try { | |
| // Always try to preload the next ready clip | |
| final nextReadyClip = queueManager.nextReadyClip; | |
| if (nextReadyClip?.base64Data != null && | |
| nextReadyClip != queueManager.currentClip && | |
| !nextReadyClip!.isPlaying) { | |
| nextController = VideoPlayerController.networkUrl( | |
| Uri.parse(nextReadyClip.base64Data!), | |
| ); | |
| await nextController.initialize(); | |
| if (isDisposed) { | |
| nextController.dispose(); | |
| return null; | |
| } | |
| // we always keep things looping. We never want any video to stop. | |
| nextController.setLooping(true); | |
| nextController.setVolume(0.0); | |
| nextController.setPlaybackSpeed(Configuration.instance.clipPlaybackSpeed); | |
| // Always ensure we're generating new clips after preloading | |
| // This is wrapped in a try-catch within ensureBufferFull now | |
| ensureBufferFull(); | |
| return nextController; | |
| } | |
| } catch (e) { | |
| // Make sure we dispose any created controller if there was an error | |
| nextController?.dispose(); | |
| debugPrint('Error preloading next clip: $e'); | |
| } | |
| // Always ensure we're generating new clips after preloading | |
| // This is wrapped in a try-catch within ensureBufferFull now | |
| if (!isDisposed) { | |
| ensureBufferFull(); | |
| } | |
| return null; | |
| } | |
| /// Update the orientation when device rotates | |
| Future<void> updateOrientation(VideoOrientation newOrientation) async { | |
| if (isDisposed) return; | |
| if (queueManager.currentOrientation == newOrientation) return; | |
| debugPrint('Updating video orientation to ${newOrientation.name}'); | |
| // Start loading progress again as we'll be regenerating clips | |
| startLoadingProgress(); | |
| // Update the orientation in the queue manager | |
| await queueManager.updateOrientation(newOrientation); | |
| } | |
| /// Dispose resources | |
| void dispose() { | |
| isDisposed = true; | |
| progressTimer?.cancel(); | |
| debugTimer?.cancel(); | |
| } | |
| } |