241 lines
6.6 KiB
Dart
241 lines
6.6 KiB
Dart
import "dart:async";
|
|
import "dart:io";
|
|
import "dart:typed_data";
|
|
import "dart:ui" as ui;
|
|
|
|
import "package:flutter/material.dart";
|
|
import "package:flutter/rendering.dart";
|
|
import "package:path/path.dart" as path;
|
|
import "package:path_provider/path_provider.dart";
|
|
import "package:quotegen_client/services/quote_api_v2.dart";
|
|
import "package:video_player/video_player.dart";
|
|
|
|
|
|
class RenderProvider extends ChangeNotifier {
|
|
|
|
Uint8List? postPreviewImage;
|
|
String? postPreviewContentType;
|
|
|
|
int renderProgress = 0;
|
|
String renderStatus = "idle";
|
|
String renderStage = "idle";
|
|
|
|
VideoPlayerController? videoController;
|
|
bool videoLoadFailed = false;
|
|
|
|
StreamSubscription? _sseSubscription;
|
|
bool _disposed = false;
|
|
|
|
|
|
void clearRender() {
|
|
if (postPreviewImage == null && renderStatus == "idle") return;
|
|
|
|
videoController?.dispose();
|
|
videoController = null;
|
|
|
|
postPreviewImage = null;
|
|
postPreviewContentType = null;
|
|
renderStatus = "idle";
|
|
renderProgress = 0;
|
|
renderStage = "idle";
|
|
videoLoadFailed = false;
|
|
|
|
if (!_disposed) notifyListeners();
|
|
}
|
|
|
|
Future<void> renderOutput(String? sessionId, bool isVideoInForm, {GlobalKey? previewKey}) async {
|
|
if (!_disposed) {
|
|
renderProgress = 0;
|
|
renderStatus = "pending";
|
|
renderStage = "idle";
|
|
notifyListeners();
|
|
}
|
|
|
|
if (isVideoInForm) {
|
|
// video still goes through the server, so no session means theres genuinely nothing we can do
|
|
if (sessionId == null) {
|
|
print("Cant render video, session isnt ready (or never got created)");
|
|
if (!_disposed) {
|
|
renderStatus = "failed";
|
|
notifyListeners();
|
|
}
|
|
return;
|
|
}
|
|
|
|
print("Starting video render for session: $sessionId");
|
|
|
|
if (!_disposed) {
|
|
renderStatus = "rendering";
|
|
renderStage = "rendering";
|
|
notifyListeners();
|
|
}
|
|
|
|
// cancel any existing sub before starting new one
|
|
await _sseSubscription?.cancel();
|
|
|
|
_sseSubscription = QuoteGeneratorApiV2.watchRenderProgress(sessionId).listen(
|
|
(progress) {
|
|
print("Render progress: ${progress.status} - ${progress.stage} - ${progress.progress}%");
|
|
|
|
if (_disposed) return;
|
|
|
|
renderProgress = progress.progress;
|
|
renderStatus = progress.status;
|
|
renderStage = progress.stage ?? "idle";
|
|
notifyListeners();
|
|
|
|
if (progress.isComplete) {
|
|
print("Render complete, fetching media...");
|
|
fetchRenderedMedia(sessionId);
|
|
} else if (progress.isFailed) {
|
|
print("Render failed: ${progress.error}");
|
|
if (!_disposed) {
|
|
renderStatus = "failed";
|
|
notifyListeners();
|
|
}
|
|
}
|
|
},
|
|
onError: (error) {
|
|
print("Error watching render progress: $error");
|
|
},
|
|
);
|
|
|
|
try {
|
|
await QuoteGeneratorApiV2.generateMedia(sessionId);
|
|
} catch (e) {
|
|
print("Error starting render: $e");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// image render - grab it straight off the live preview widget, dont need the server for this bit anymore
|
|
try {
|
|
if (previewKey == null) {
|
|
throw Exception("no preview key given, cant capture anything");
|
|
}
|
|
|
|
// renderStatus flipped to pending up top already, which swaps the ui back to the live
|
|
// tweet card - just gotta wait for that frame to actually paint before we grab it
|
|
await WidgetsBinding.instance.endOfFrame;
|
|
|
|
final bytes = await _capturePreview(previewKey);
|
|
|
|
if (_disposed) return;
|
|
|
|
videoController?.dispose();
|
|
videoController = null;
|
|
|
|
postPreviewImage = bytes;
|
|
postPreviewContentType = "image/png";
|
|
videoLoadFailed = false;
|
|
renderStatus = "completed";
|
|
renderProgress = 100;
|
|
renderStage = "completed";
|
|
notifyListeners();
|
|
|
|
} catch (e) {
|
|
print("Error rendering: $e");
|
|
if (!_disposed) {
|
|
renderStatus = "failed";
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
// rasterizes a repaint boundary to png, scaled to a fixed resolution regardless of its onscreen size
|
|
Future<Uint8List> _capturePreview(GlobalKey key, {double targetSize = 1080}) async {
|
|
final renderObj = key.currentContext?.findRenderObject();
|
|
|
|
if (renderObj is! RenderRepaintBoundary) {
|
|
throw Exception("preview widget isnt mounted, nothing to capture");
|
|
}
|
|
|
|
final logicalSize = renderObj.size;
|
|
final pixelRatio = logicalSize.shortestSide == 0 ? 1.0 : targetSize / logicalSize.shortestSide;
|
|
|
|
final image = await renderObj.toImage(pixelRatio: pixelRatio);
|
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
image.dispose();
|
|
|
|
if (byteData == null) {
|
|
throw Exception("failed to encode captured png");
|
|
}
|
|
|
|
return byteData.buffer.asUint8List();
|
|
}
|
|
|
|
Future<void> fetchRenderedMedia(String sessionId) async {
|
|
try {
|
|
MediaResponse media = await QuoteGeneratorApiV2.generateMedia(sessionId);
|
|
|
|
if (_disposed) return;
|
|
|
|
videoController?.dispose();
|
|
videoController = null;
|
|
|
|
postPreviewImage = media.bytes;
|
|
postPreviewContentType = media.contentType;
|
|
videoLoadFailed = false;
|
|
renderStatus = "completed";
|
|
renderProgress = 100;
|
|
renderStage = "completed";
|
|
notifyListeners();
|
|
|
|
if (media.isVideo) {
|
|
await initializeVideoPlayer(media.bytes);
|
|
}
|
|
|
|
} catch (e) {
|
|
print("Error fetching rendered media: $e");
|
|
if (!_disposed) {
|
|
renderStatus = "failed";
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> initializeVideoPlayer(Uint8List videoBytes) async {
|
|
try {
|
|
videoLoadFailed = false;
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
final videoPath = path.join(tempDir.path, "preview_video_${DateTime.now().millisecondsSinceEpoch}.mp4");
|
|
final file = File(videoPath);
|
|
await file.writeAsBytes(videoBytes);
|
|
|
|
print("Video saved to: $videoPath (${videoBytes.length} bytes)");
|
|
|
|
videoController = VideoPlayerController.file(file);
|
|
|
|
videoController!.addListener(() {
|
|
if (!_disposed) notifyListeners();
|
|
});
|
|
|
|
await videoController!.initialize();
|
|
|
|
videoController!.setLooping(true);
|
|
videoController!.play();
|
|
|
|
if (!_disposed) {
|
|
videoLoadFailed = false;
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
print("Error initializing video player: $e");
|
|
if (!_disposed) {
|
|
videoLoadFailed = true;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
_sseSubscription?.cancel();
|
|
videoController?.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|