Skip to main content

Documentation Index

Fetch the complete documentation index at: https://learn.social.plus/llms.txt

Use this file to discover all available pages before exploring further.

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.