最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

amazon web services - How to handle dynamic updates for a remote system with Flutter? - Stack Overflow

programmeradmin3浏览0评论

I have an project coded in Flutter where the main system runs on a remote computer called totem. The system have some gifs running as the background decoration of the app. To update the appearance of the totem I use an admin dashboard to send the updates using SNS and each totem reads the message in the SQS queue. As the system should not rely on internet I download the pictures in data/flutter_assets/lib/assets/videos folder because the executable file just can read that filepath (There's no lib/assets after building). The download works without problems but the issue arises when Flutter needs to update the Image.assets(), because it reads the same file all over again instead of the updated one. The gifs are stored with the same name, it just needs to reload the assets but as I am a beginner with Flutter, I dont really know how to approach to this problem. This a sample screen:

// lib/screens/welcome_screen.dart

import 'dart:async';
import 'dart:convert';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../config/app_config.dart';
import '../mixins/activity_timer_mixin.dart';
import './instructions_screen.dart';

class WelcomeScreen extends StatefulWidget {
  final CameraDescription camera;

  const WelcomeScreen({super.key, required this.camera});

  @override
  State<WelcomeScreen> createState() => _WelcomeScreenState();
}

class _WelcomeScreenState extends State<WelcomeScreen> with ActivityTimerMixin {
  Timer? _timer;
  bool _isProcessing = false;

  @override
  void initState() {
    super.initState();
    _initializeSharedController();
  }

  Future<void> _initializeSharedController() async {
    if (sharedController != null) return;

    sharedController = CameraController(
      widget.camera,
      AppConfig.defaultCameraResolution,
      enableAudio: AppConfig.enableAudio,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    try {
      await sharedController!.initialize();
      if (mounted) {
        setState(() {});
        _startGestureDetection();
      }
    } catch (e) {
      print('Error initializing camera: $e');
    }
  }

  void _startGestureDetection() {
    _timer?.cancel();
    _timer = Timer.periodic(
      AppConfig.gestureDetectionInterval,
      (timer) {
        if (!_isProcessing) {
          _detectGesture();
        }
      },
    );
  }

  Future<void> _detectGesture() async {
    if (sharedController == null || !sharedController!.value.isInitialized) return;

    try {
      _isProcessing = true;
      final image = await sharedController!.takePicture();
      final bytes = await image.readAsBytes();
      
      var request = http.MultipartRequest(
        'POST',
        Uri.parse(AppConfig.detectEndpoint),
      );

      request.files.add(
        http.MultipartFile.fromBytes(
          'file',
          bytes,
          filename: 'image.jpg',
        ),
      );

      var streamedResponse = await request.send();
      var response = await http.Response.fromStream(streamedResponse);

      if (response.statusCode == 200) {
        var decodedResponse = json.decode(response.body);
        var detections = decodedResponse['detections'] as List;
        
        if (detections.isNotEmpty) {
          var highestConfidenceDetection = detections.reduce((curr, next) =>
            (curr['confidence'] as double) > (next['confidence'] as double) ? curr : next);
          
          String gesture = highestConfidenceDetection['class'];
          double confidence = highestConfidenceDetection['confidence'] * 100;
          
          if (confidence >= AppConfig.confidenceThreshold) {
            if (gesture == 'hi' && mounted) {
              _timer?.cancel();
              
              if (!mounted) return;
              
              Navigator.pushReplacement(
                context,
                MaterialPageRoute(
                  builder: (context) => InstructionsScreen(
                    camera: widget.camera,
                    sharedController: sharedController,
                  ),
                ),
              );
            }
          }
        }
      }
    } catch (e) {
      print('Error during gesture detection: $e');
    } finally {
      _isProcessing = false;
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        fit: StackFit.expand,
        children: [
          // Always use Image.asset for the background GIF
          Image.asset(
            AppConfig.welcomeBackgroundPath,
            fit: BoxFit.cover,
            gaplessPlayback: true,
            errorBuilder: (context, error, stackTrace) {
              print('Error loading background: $error');
              // Fallback to bundled asset
              return Image.asset(
                AppConfig.welcomeBackgroundPath,
                fit: BoxFit.cover,
                gaplessPlayback: true,
              );
            },
          ),
          
          // Semi-transparent overlay for better readability
          Container(
            color: Colors.black.withOpacity(0.4),
          ),
          
          // Main content
          SafeArea(
            child: Center(
              child: LayoutBuilder(
                builder: (context, constraints) {
                  // Calculate responsive font size based on screen width
                  double fontSize = constraints.maxWidth * 0.04; // 4% of screen width
                  
                  return Padding(
                    padding: EdgeInsets.symmetric(
                      horizontal: constraints.maxWidth * 0.1, // 10% padding
                    ),
                    child: Text(
                      'Imita el gesto para empezar...',
                      style: TextStyle(
                        fontSize: fontSize,
                        fontWeight: FontWeight.bold,
                        color: Colors.white,
                        letterSpacing: 1.2,
                        shadows: [
                          Shadow(
                            offset: const Offset(2.0, 2.0),
                            blurRadius: 3.0,
                            color: Colors.black.withOpacity(0.5),
                          ),
                        ],
                      ),
                      textAlign: TextAlign.center,
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

This is the app_config:

// lib/config/app_config.dart

import 'dart:io';
import 'dart:convert';
import 'package:camera/camera.dart';
class AppConfig {
  // Default background media paths
  // These point to the assets bundled with the app
  static String welcomeBackgroundPath = 'lib/assets/videos/main_view.gif';
  static String defaultBackgroundPath = 'lib/assets/videos/bg_view.gif';
  static String loadingVideoPath = 'lib/assets/videos/ad_view.gif';
  
  // Flags to track if we're using downloaded assets vs bundled assets
  static bool usingDownloadedMainView = false;
  static bool usingDownloadedBgView = false;
  static bool usingDownloadedAdView = false;
  
  // API endpoints
  static const String baseApiUrl = 'http://localhost:8000';
  static const String detectEndpoint = '$baseApiUrl/detect';
  static const String uploadImageEndpoint = '$baseApiUrl/upload-image';
  static const String uploadImageS3Endpoint = '$baseApiUrl/upload-image-s3';
  static const String generateImageEndpoint = '$baseApiUrl/generate-image';

  // Gesture detection settings
  static const double confidenceThreshold = 77.0;
  static const Duration gestureDetectionInterval = Duration(milliseconds: 350);
  
  // Camera settings
  static const ResolutionPreset defaultCameraResolution = ResolutionPreset.max;
  static const bool enableAudio = false;
  
  // Initialize app configuration by checking for appearance config
  static Future<void> initAppConfig() async {
    try {
      // Try to load appearance_config.json first
      final currentDir = Directory.current;
      final String configPath = '${currentDir.path}/appearance_config.json';
      final configFile = File(configPath);
      
      print('Checking for appearance config at: $configPath');
      
      if (await configFile.exists()) {
        try {
          final String configContent = await configFile.readAsString();
          final Map<String, dynamic> config = jsonDecode(configContent);
          
          if (config.containsKey('appearance')) {
            final appearance = config['appearance'];
            
            // Check main_view GIF - should now be an asset path like "videos/main_view.gif"
            final String? mainViewPath = appearance['main_view'];
            if (mainViewPath != null && mainViewPath.isNotEmpty && mainViewPath.startsWith('videos/')) {
              welcomeBackgroundPath = mainViewPath;
              usingDownloadedMainView = true;
              print('Using downloaded main_view GIF: $mainViewPath');
            }
            
            // Check bg_view GIF
            final String? bgViewPath = appearance['bg_view'];
            if (bgViewPath != null && bgViewPath.isNotEmpty && bgViewPath.startsWith('videos/')) {
              defaultBackgroundPath = bgViewPath;
              usingDownloadedBgView = true;
              print('Using downloaded bg_view GIF: $bgViewPath');
            }
            
            // Check ad_view GIF
            final String? adViewPath = appearance['ad_view'];
            if (adViewPath != null && adViewPath.isNotEmpty && adViewPath.startsWith('videos/')) {
              loadingVideoPath = adViewPath;
              usingDownloadedAdView = true;
              print('Using downloaded ad_view GIF: $adViewPath');
            }
          }
        } catch (e) {
          print('Error parsing appearance_config.json: $e');
        }
      } else {
        print('No appearance_config.json found at $configPath');
      }
      
      // Log final configuration
      print('AppConfig initialized:');
      print('- Welcome background: $welcomeBackgroundPath (downloaded: $usingDownloadedMainView)');
      print('- Default background: $defaultBackgroundPath (downloaded: $usingDownloadedBgView)');
      print('- Loading video: $loadingVideoPath (downloaded: $usingDownloadedAdView)');
      
    } catch (e) {
      print('Error initializing app config: $e');
      // Keep using default paths on error
    }
  }
}

// Gesture mappings and their actions
class GestureConfig {
  // Photo Review Screen gestures
  static const photoReviewGestures = {
    'thumbsup': GestureAction.usePhoto,
    'thumbsdown': GestureAction.retakePhoto,
  };

  // Welcome Screen gestures
  static const welcomeScreenGestures = {
    'hi': GestureAction.startApp,
  };

  // Option Screen gestures
  static const optionScreenGestures = {
    'fist': GestureAction.selectOption1,
    'peace': GestureAction.selectOption2,
    'heart': GestureAction.showGuide,
  };

  // Results Screen gestures
  static const resultScreenGestures = {
    'thumbsup': GestureAction.generateQR,
    'thumbsdown': GestureAction.regenerate,
    'fist': GestureAction.goBack,
  };
}

enum GestureAction {
  usePhoto,
  retakePhoto,
  startApp,
  selectOption1,
  selectOption2,
  showGuide,
  generateQR,
  regenerate,
  goBack,
}

This is a sample response of the update system:

{
  "appearance": {
    "main_view": "lib/assets/videos/main_view.gif",
    "bg_view": "lib/assets/videos/bg_view.gif",
    "ad_view": "lib/assets/videos/ad_view.gif",
    "timestamp": "2025-03-27T09:21:21.357Z"
  }
}
发布评论

评论列表(0)

  1. 暂无评论