Skip to main content

Flutter Implementation Guide

This comprehensive guide covers everything you need to integrate social.plus Video SDK into your Flutter application, including live streaming, video playback, and real-time notifications.

Prerequisites

  • Flutter 3.0 or higher
  • Dart 2.17 or higher
  • iOS 12+ (for iOS apps)
  • Android API level 21+ (for Android apps)
  • Valid social.plus API credentials

Installation & Setup

Package Installation

Add the required dependencies to your pubspec.yaml:
dependencies:
  flutter:
    sdk: flutter
  
  # social.plus Video SDK dependencies
  amity_sdk: ^6.0.0
  amity_video_player: ^0.0.1
  
  # Additional dependencies for enhanced functionality
  permission_handler: ^10.4.3
  video_player: ^2.7.0
  camera: ^0.10.5
  path_provider: ^2.1.0
  http: ^1.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
Then run:
flutter pub get

Platform Configuration

1. Configure Build Settings

Update android/app/build.gradle:
android {
    compileSdkVersion 34
    ndkVersion flutter.ndkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 34
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
}

2. Configure Permissions

Add permissions to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Camera and Audio Permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    
    <!-- Network Permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    
    <!-- Storage Permissions -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
    <!-- Hardware Features -->
    <uses-feature android:name="android.hardware.camera" android:required="true" />
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
    <uses-feature android:name="android.hardware.microphone" android:required="true" />

    <application
        android:label="Your App Name"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        
        <!-- Add network security config for HTTP traffic (if needed) -->
        <meta-data
            android:name="android.content.NETWORK_SECURITY_CONFIG"
            android:resource="@xml/network_security_config" />
            
        <!-- Your activities -->
    </application>
</manifest>

3. Network Security Configuration

Create android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">your-streaming-domain.com</domain>
    </domain-config>
</network-security-config>

SDK Initialization

Basic Setup

// lib/services/social_plus_service.dart
import 'package:amity_sdk/amity_sdk.dart';

class SocialPlusService {
  static final SocialPlusService _instance = SocialPlusService._internal();
  factory SocialPlusService() => _instance;
  SocialPlusService._internal();

  AmitySDK? _sdk;
  bool _isInitialized = false;

  Future<void> initialize({
    required String apiKey,
    required String httpUrl,
    required String socketUrl,
  }) async {
    try {
      _sdk = AmitySDK();
      
      await _sdk!.setup(
        option: AmitySDKSetupOption(
          apiKey: apiKey,
          httpEndpoint: httpUrl,
          socketEndpoint: socketUrl,
        ),
      );
      
      _isInitialized = true;
      print('social.plus SDK initialized successfully');
    } catch (error) {
      print('Failed to initialize social.plus SDK: $error');
      rethrow;
    }
  }

  Future<AmityUser> login({
    required String userId,
    String? displayName,
    String? authToken,
  }) async {
    if (!_isInitialized) {
      throw Exception('SDK not initialized. Call initialize() first.');
    }

    try {
      final user = await _sdk!.login(userId)
          .displayName(displayName ?? '')
          .authToken(authToken)
          .submit();
      
      print('User logged in successfully: ${user.userId}');
      return user;
    } catch (error) {
      print('Login failed: $error');
      rethrow;
    }
  }

  Future<void> logout() async {
    if (_sdk != null) {
      await _sdk!.logout();
    }
  }

  AmitySDK? get sdk => _sdk;
  bool get isInitialized => _isInitialized;
}

App Initialization

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:amity_video_player/amity_video_player.dart';
import 'services/social_plus_service.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'social.plus Video App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SplashScreen(),
    );
  }
}

class SplashScreen extends StatefulWidget {
  @override
  _SplashScreenState createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  final SocialPlusService _socialPlusService = SocialPlusService();

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

  Future<void> _initializeApp() async {
    try {
      // Initialize social.plus SDK
      await _socialPlusService.initialize(
        apiKey: 'your-api-key',
        httpUrl: 'https://api.amity.co',
        socketUrl: 'wss://api.amity.co',
      );

      // Login user
      await _socialPlusService.login(
        userId: 'user-123',
        displayName: 'John Doe',
      );

      // Setup video player
      await AmityVideoPlayerClient.setup();

      // Navigate to main screen
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (context) => HomeScreen()),
      );
    } catch (error) {
      // Handle initialization error
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to initialize app: $error')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('Initializing social.plus Video SDK...'),
          ],
        ),
      ),
    );
  }
}

Live Streaming Implementation

Stream Management Service

// lib/services/stream_service.dart
import 'package:amity_sdk/amity_sdk.dart';

class StreamService {
  final AmityStreamRepository _streamRepository;

  StreamService() : _streamRepository = AmitySDK.newStreamRepository();

  Future<AmityStream> createStream({
    required String title,
    String? description,
    String? thumbnailFileId,
    bool isSecure = false,
    Map<String, dynamic>? metadata,
  }) async {
    try {
      final stream = await _streamRepository
          .createStream()
          .title(title)
          .description(description ?? '')
          .thumbnailFileId(thumbnailFileId)
          .isSecure(isSecure)
          .metadata(metadata ?? {})
          .create();

      print('Stream created successfully: ${stream.streamId}');
      return stream;
    } catch (error) {
      print('Failed to create stream: $error');
      rethrow;
    }
  }

  Future<void> updateStream({
    required String streamId,
    String? title,
    String? description,
    String? thumbnailFileId,
    Map<String, dynamic>? metadata,
  }) async {
    try {
      await _streamRepository
          .updateStream(streamId)
          .title(title)
          .description(description)
          .thumbnailFileId(thumbnailFileId)
          .metadata(metadata ?? {})
          .update();

      print('Stream updated successfully: $streamId');
    } catch (error) {
      print('Failed to update stream: $error');
      rethrow;
    }
  }

  Future<void> deleteStream(String streamId) async {
    try {
      await _streamRepository.deleteStream(streamId);
      print('Stream deleted successfully: $streamId');
    } catch (error) {
      print('Failed to delete stream: $error');
      rethrow;
    }
  }

  AmityStreamLiveCollection getStreams({
    List<AmityStreamStatus>? statuses,
    AmityStreamSortOption sortOption = AmityStreamSortOption.CREATED_AT,
  }) {
    return _streamRepository
        .getStreams()
        .setStatus(statuses ?? [])
        .sortBy(sortOption)
        .build();
  }

  AmityStreamLiveObject getStreamById(String streamId) {
    return _streamRepository.getStreamById(streamId);
  }
}

Broadcasting Screen

// lib/screens/broadcast_screen.dart
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:permission_handler/permission_handler.dart';
import '../services/stream_service.dart';

class BroadcastScreen extends StatefulWidget {
  @override
  _BroadcastScreenState createState() => _BroadcastScreenState();
}

class _BroadcastScreenState extends State<BroadcastScreen> {
  final StreamService _streamService = StreamService();
  
  CameraController? _cameraController;
  List<CameraDescription> _cameras = [];
  bool _isRecording = false;
  bool _isCameraInitialized = false;
  AmityStream? _currentStream;

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

  Future<void> _initializeCamera() async {
    // Request permissions
    final cameraPermission = await Permission.camera.request();
    final microphonePermission = await Permission.microphone.request();

    if (cameraPermission.isGranted && microphonePermission.isGranted) {
      _cameras = await availableCameras();
      
      if (_cameras.isNotEmpty) {
        _cameraController = CameraController(
          _cameras[0],
          ResolutionPreset.high,
          enableAudio: true,
        );

        await _cameraController!.initialize();
        
        if (mounted) {
          setState(() {
            _isCameraInitialized = true;
          });
        }
      }
    } else {
      _showPermissionDialog();
    }
  }

  Future<void> _startBroadcast() async {
    if (_cameraController == null) return;

    try {
      // Create stream
      _currentStream = await _streamService.createStream(
        title: 'Live Broadcast from Flutter',
        description: 'Broadcasting live using social.plus Flutter SDK',
        isSecure: false,
      );

      // Start recording
      await _cameraController!.startVideoRecording();
      
      setState(() {
        _isRecording = true;
      });

      _showSnackBar('Broadcast started successfully!');
    } catch (error) {
      _showSnackBar('Failed to start broadcast: $error');
    }
  }

  Future<void> _stopBroadcast() async {
    if (_cameraController == null || !_isRecording) return;

    try {
      // Stop recording
      final videoFile = await _cameraController!.stopVideoRecording();
      
      setState(() {
        _isRecording = false;
      });

      _showSnackBar('Broadcast stopped. Video saved: ${videoFile.path}');
    } catch (error) {
      _showSnackBar('Failed to stop broadcast: $error');
    }
  }

  Future<void> _switchCamera() async {
    if (_cameras.length < 2) return;

    final currentCameraIndex = _cameras.indexWhere(
      (camera) => camera.lensDirection == _cameraController!.description.lensDirection,
    );
    
    final newCameraIndex = (currentCameraIndex + 1) % _cameras.length;
    
    await _cameraController!.dispose();
    
    _cameraController = CameraController(
      _cameras[newCameraIndex],
      ResolutionPreset.high,
      enableAudio: true,
    );

    await _cameraController!.initialize();
    setState(() {});
  }

  void _showPermissionDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Permissions Required'),
        content: Text('Camera and microphone permissions are required for broadcasting.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              openAppSettings();
            },
            child: Text('Open Settings'),
          ),
        ],
      ),
    );
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Live Broadcast'),
        backgroundColor: Colors.red,
      ),
      body: Column(
        children: [
          // Camera Preview
          Expanded(
            child: _isCameraInitialized
                ? CameraPreview(_cameraController!)
                : Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircularProgressIndicator(),
                        SizedBox(height: 16),
                        Text('Initializing camera...'),
                      ],
                    ),
                  ),
          ),
          
          // Controls
          Container(
            padding: EdgeInsets.symmetric(vertical: 20, horizontal: 16),
            color: Colors.black87,
            child: Column(
              children: [
                if (_currentStream != null)
                  Text(
                    'Stream: ${_currentStream!.title}',
                    style: TextStyle(color: Colors.white, fontSize: 16),
                  ),
                
                SizedBox(height: 16),
                
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    // Start/Stop Broadcast Button
                    ElevatedButton(
                      onPressed: _isCameraInitialized
                          ? (_isRecording ? _stopBroadcast : _startBroadcast)
                          : null,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _isRecording ? Colors.red : Colors.green,
                        shape: CircleBorder(),
                        padding: EdgeInsets.all(20),
                      ),
                      child: Icon(
                        _isRecording ? Icons.stop : Icons.play_arrow,
                        color: Colors.white,
                        size: 30,
                      ),
                    ),
                    
                    // Switch Camera Button
                    ElevatedButton(
                      onPressed: _isCameraInitialized && _cameras.length > 1
                          ? _switchCamera
                          : null,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.blue,
                        shape: CircleBorder(),
                        padding: EdgeInsets.all(16),
                      ),
                      child: Icon(
                        Icons.switch_camera,
                        color: Colors.white,
                        size: 24,
                      ),
                    ),
                  ],
                ),
                
                SizedBox(height: 8),
                
                Text(
                  _isRecording ? 'LIVE' : 'Ready to broadcast',
                  style: TextStyle(
                    color: _isRecording ? Colors.red : Colors.white,
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Video Playback Implementation

Video Player Screen

// lib/screens/video_player_screen.dart
import 'package:flutter/material.dart';
import 'package:amity_video_player/amity_video_player.dart';
import '../services/stream_service.dart';

class VideoPlayerScreen extends StatefulWidget {
  final String streamId;

  const VideoPlayerScreen({Key? key, required this.streamId}) : super(key: key);

  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  final StreamService _streamService = StreamService();
  
  AmityVideoController? _videoController;
  AmityStream? _stream;
  bool _isLoading = true;
  bool _hasError = false;
  String? _errorMessage;

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

  Future<void> _loadStream() async {
    try {
      final streamLiveObject = _streamService.getStreamById(widget.streamId);
      
      streamLiveObject.listen().listen((amityStream) {
        if (mounted) {
          setState(() {
            _stream = amityStream;
            _isLoading = false;
          });
          
          if (amityStream != null) {
            _initializeVideoPlayer(amityStream);
          }
        }
      });
      
    } catch (error) {
      setState(() {
        _isLoading = false;
        _hasError = true;
        _errorMessage = error.toString();
      });
    }
  }

  void _initializeVideoPlayer(AmityStream stream) {
    _videoController = AmityVideoController(
      streamId: stream.streamId!,
      onPlayerStateChanged: (state) {
        print('Player state changed: $state');
      },
      onError: (error) {
        print('Video player error: $error');
        setState(() {
          _hasError = true;
          _errorMessage = error;
        });
      },
    );
  }

  Future<void> _playVideo() async {
    if (_videoController != null) {
      await _videoController!.play();
    }
  }

  Future<void> _pauseVideo() async {
    if (_videoController != null) {
      await _videoController!.pause();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_stream?.title ?? 'Video Player'),
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('Loading stream...'),
          ],
        ),
      );
    }

    if (_hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.error_outline,
              size: 64,
              color: Colors.red,
            ),
            SizedBox(height: 16),
            Text(
              'Error loading stream',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 8),
            Text(
              _errorMessage ?? 'Unknown error occurred',
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.grey[600]),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  _isLoading = true;
                  _hasError = false;
                  _errorMessage = null;
                });
                _loadStream();
              },
              child: Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (_stream == null) {
      return Center(
        child: Text('Stream not found'),
      );
    }

    return Column(
      children: [
        // Video Player
        Expanded(
          child: Container(
            color: Colors.black,
            child: _videoController != null
                ? AmityVideoPlayer(controller: _videoController!)
                : Center(
                    child: Text(
                      'Initializing player...',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
          ),
        ),
        
        // Stream Info and Controls
        Container(
          padding: EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                _stream!.title ?? 'Untitled Stream',
                style: TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              
              if (_stream!.description != null && _stream!.description!.isNotEmpty)
                Padding(
                  padding: EdgeInsets.only(top: 8),
                  child: Text(
                    _stream!.description!,
                    style: TextStyle(
                      color: Colors.grey[600],
                      fontSize: 14,
                    ),
                  ),
                ),
              
              SizedBox(height: 16),
              
              Row(
                children: [
                  Container(
                    padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      color: _getStatusColor(_stream!.status),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      _stream!.status.toString().split('.').last.toUpperCase(),
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  
                  Spacer(),
                  
                  // Play/Pause Button
                  ElevatedButton.icon(
                    onPressed: _playVideo,
                    icon: Icon(Icons.play_arrow),
                    label: Text('Play'),
                  ),
                  
                  SizedBox(width: 8),
                  
                  ElevatedButton.icon(
                    onPressed: _pauseVideo,
                    icon: Icon(Icons.pause),
                    label: Text('Pause'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  Color _getStatusColor(AmityStreamStatus status) {
    switch (status) {
      case AmityStreamStatus.LIVE:
        return Colors.red;
      case AmityStreamStatus.RECORDED:
        return Colors.green;
      case AmityStreamStatus.ENDED:
        return Colors.orange;
      default:
        return Colors.grey;
    }
  }
}

Push Notifications

For comprehensive push notification setup, refer to our Push Notifications Guide.

Basic Notification Setup

// lib/services/notification_service.dart
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

class NotificationService {
  static final NotificationService _instance = NotificationService._internal();
  factory NotificationService() => _instance;
  NotificationService._internal();

  final FlutterLocalNotificationsPlugin _localNotifications = 
      FlutterLocalNotificationsPlugin();
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  Future<void> initialize() async {
    // Initialize local notifications
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

    const initSettings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );

    await _localNotifications.initialize(
      initSettings,
      onDidReceiveNotificationResponse: _onNotificationTapped,
    );

    // Initialize Firebase Messaging
    await _setupFirebaseMessaging();
  }

  Future<void> _setupFirebaseMessaging() async {
    // Request permissions
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
      
      // Get FCM token
      String? token = await _firebaseMessaging.getToken();
      print('FCM Token: $token');
      
      // Listen to messages
      FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
      FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
    }
  }

  void _handleForegroundMessage(RemoteMessage message) {
    print('Received message: ${message.notification?.title}');
    
    if (message.data['type'] == 'stream_notification') {
      _showStreamNotification(message);
    }
  }

  void _handleNotificationTap(RemoteMessage message) {
    print('Notification tapped: ${message.data}');
    // Handle navigation based on notification data
  }

  void _onNotificationTapped(NotificationResponse response) {
    print('Local notification tapped: ${response.payload}');
  }

  Future<void> _showStreamNotification(RemoteMessage message) async {
    const androidDetails = AndroidNotificationDetails(
      'stream_channel',
      'Stream Notifications',
      channelDescription: 'Notifications for live streams',
      importance: Importance.high,
      priority: Priority.high,
    );

    const iosDetails = DarwinNotificationDetails();

    const notificationDetails = NotificationDetails(
      android: androidDetails,
      iOS: iosDetails,
    );

    await _localNotifications.show(
      DateTime.now().millisecondsSinceEpoch ~/ 1000,
      message.notification?.title ?? 'Stream Update',
      message.notification?.body ?? 'Check out this stream!',
      notificationDetails,
      payload: message.data['streamId'],
    );
  }
}

Error Handling & Troubleshooting

Error Handler Utility

// lib/utils/error_handler.dart
class VideoSDKErrorHandler {
  static String getErrorMessage(dynamic error) {
    if (error is AmityException) {
      switch (error.code) {
        case AmityErrorCode.NETWORK_ERROR:
          return 'Network connection issue. Please check your internet.';
        case AmityErrorCode.PERMISSION_DENIED:
          return 'Camera and microphone permissions are required.';
        case AmityErrorCode.STREAM_NOT_FOUND:
          return 'The requested stream is not available.';
        case AmityErrorCode.INVALID_PARAMETER:
          return 'Invalid stream parameters provided.';
        default:
          return 'An unexpected error occurred: ${error.message}';
      }
    }
    
    return 'Unknown error: $error';
  }

  static Future<T?> executeWithRetry<T>(
    Future<T> Function() operation, {
    int maxRetries = 3,
    Duration delay = const Duration(seconds: 1),
  }) async {
    for (int i = 0; i < maxRetries; i++) {
      try {
        return await operation();
      } catch (error) {
        if (i == maxRetries - 1) {
          print('Operation failed after $maxRetries attempts: $error');
          rethrow;
        }
        
        print('Attempt ${i + 1} failed, retrying in ${delay.inSeconds}s...');
        await Future.delayed(delay);
        delay *= 2; // Exponential backoff
      }
    }
    return null;
  }
}

Testing

Widget Testing Example

// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/screens/video_player_screen.dart';
import 'package:your_app/services/stream_service.dart';

class MockStreamService extends Mock implements StreamService {}

void main() {
  group('Video Player Screen Tests', () {
    late MockStreamService mockStreamService;

    setUp(() {
      mockStreamService = MockStreamService();
    });

    testWidgets('should display loading state initially', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: VideoPlayerScreen(streamId: 'test-stream-id'),
        ),
      );

      expect(find.text('Loading stream...'), findsOneWidget);
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
    });

    testWidgets('should display stream title when loaded', (WidgetTester tester) async {
      // Mock stream data
      final mockStream = AmityStream()
        ..streamId = 'test-stream-id'
        ..title = 'Test Stream Title';

      when(mockStreamService.getStreamById('test-stream-id'))
          .thenAnswer((_) => Stream.value(mockStream));

      await tester.pumpWidget(
        MaterialApp(
          home: VideoPlayerScreen(streamId: 'test-stream-id'),
        ),
      );

      await tester.pump();

      expect(find.text('Test Stream Title'), findsOneWidget);
    });
  });
}

Performance Optimization

Best Practices

// lib/utils/performance_utils.dart
class PerformanceUtils {
  static Map<String, dynamic> getOptimalVideoSettings() {
    // Get device capabilities
    final screenSize = WidgetsBinding.instance.platformDispatcher.views.first.physicalSize;
    final pixelRatio = WidgetsBinding.instance.platformDispatcher.views.first.devicePixelRatio;
    
    final screenWidth = screenSize.width / pixelRatio;
    final screenHeight = screenSize.height / pixelRatio;

    // Determine optimal settings based on screen size
    if (screenWidth >= 1920) {
      return {
        'resolution': ResolutionPreset.veryHigh,
        'bitrate': 4000000,
        'frameRate': 30,
      };
    } else if (screenWidth >= 1280) {
      return {
        'resolution': ResolutionPreset.high,
        'bitrate': 2000000,
        'frameRate': 30,
      };
    } else {
      return {
        'resolution': ResolutionPreset.medium,
        'bitrate': 1000000,
        'frameRate': 24,
      };
    }
  }

  static void optimizeMemoryUsage() {
    // Force garbage collection
    // Note: This should be used sparingly in production
    // GCUtils.forceGC(); // Platform-specific implementation needed
  }
}

Troubleshooting

For detailed troubleshooting guides, see:

Common Flutter Issues

  1. Camera initialization fails: Check permissions in AndroidManifest.xml and Info.plist
  2. Video playback issues: Ensure proper network configuration and stream availability
  3. Build failures: Verify Flutter and Dart versions, run flutter clean and flutter pub get
  4. iOS deployment issues: Update Podfile and run pod install

Next Steps

Support

Need help? Contact our support team or visit our community forum for assistance with Flutter implementation.