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 yourpubspec.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
flutter pub get
Platform Configuration
- Android
- iOS
1. Configure Build Settings
Updateandroid/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 toandroid/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
Createandroid/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>
1. Configure iOS Deployment Target
Updateios/Podfile:platform :ios, '12.0'
# Add this if not present
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end
2. Configure Permissions
Add permissions toios/Runner/Info.plist:<dict>
<!-- Existing keys... -->
<!-- Camera Permission -->
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to broadcast live streams and record videos</string>
<!-- Microphone Permission -->
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for live streaming audio and video recording</string>
<!-- Photo Library Permission -->
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to save and share videos</string>
<!-- Photo Library Add Permission -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs permission to save videos to your photo library</string>
<!-- Background Modes (for streaming) -->
<key>UIBackgroundModes</key>
<array>
<string>background-audio</string>
<string>background-processing</string>
</array>
</dict>
3. Install iOS Dependencies
cd ios && pod install
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
- Basic Broadcasting
- Advanced Broadcasting
// 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,
),
),
],
),
),
],
),
);
}
}
// lib/screens/advanced_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 AdvancedBroadcastScreen extends StatefulWidget {
@override
_AdvancedBroadcastScreenState createState() => _AdvancedBroadcastScreenState();
}
class _AdvancedBroadcastScreenState extends State<AdvancedBroadcastScreen>
with TickerProviderStateMixin {
final StreamService _streamService = StreamService();
CameraController? _cameraController;
List<CameraDescription> _cameras = [];
bool _isRecording = false;
bool _isCameraInitialized = false;
AmityStream? _currentStream;
// Advanced features
AnimationController? _recordingAnimationController;
Timer? _broadcastTimer;
Duration _broadcastDuration = Duration.zero;
int _viewerCount = 0;
bool _isFlashOn = false;
ResolutionPreset _currentResolution = ResolutionPreset.high;
@override
void initState() {
super.initState();
_recordingAnimationController = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
_initializeCamera();
}
Future<void> _initializeCamera() async {
final permissions = await [
Permission.camera,
Permission.microphone,
Permission.storage,
].request();
if (permissions.values.every((status) => status.isGranted)) {
_cameras = await availableCameras();
if (_cameras.isNotEmpty) {
await _setupCamera(_cameras[0]);
}
} else {
_showPermissionDialog();
}
}
Future<void> _setupCamera(CameraDescription camera) async {
_cameraController = CameraController(
camera,
_currentResolution,
enableAudio: true,
imageFormatGroup: ImageFormatGroup.jpeg,
);
try {
await _cameraController!.initialize();
if (mounted) {
setState(() {
_isCameraInitialized = true;
});
}
} catch (error) {
print('Error initializing camera: $error');
}
}
Future<void> _startAdvancedBroadcast() async {
if (_cameraController == null) return;
try {
// Create stream with metadata
_currentStream = await _streamService.createStream(
title: 'Advanced Live Broadcast',
description: 'High-quality broadcast with advanced features',
isSecure: false,
metadata: {
'resolution': _currentResolution.toString(),
'platform': 'flutter',
'features': ['hd_quality', 'flash_support', 'camera_switch']
},
);
// Start recording with advanced settings
await _cameraController!.startVideoRecording();
// Start animation and timer
_recordingAnimationController!.repeat();
_startBroadcastTimer();
setState(() {
_isRecording = true;
});
_showSnackBar('Advanced broadcast started!');
} catch (error) {
_showSnackBar('Failed to start broadcast: $error');
}
}
void _startBroadcastTimer() {
_broadcastTimer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_broadcastDuration = Duration(seconds: timer.tick);
// Simulate viewer count changes
_viewerCount = 10 + (timer.tick % 50);
});
});
}
Future<void> _stopAdvancedBroadcast() async {
if (_cameraController == null || !_isRecording) return;
try {
final videoFile = await _cameraController!.stopVideoRecording();
_recordingAnimationController!.stop();
_broadcastTimer?.cancel();
setState(() {
_isRecording = false;
_broadcastDuration = Duration.zero;
_viewerCount = 0;
});
_showSnackBar('Broadcast ended. Duration: ${_formatDuration(_broadcastDuration)}');
} catch (error) {
_showSnackBar('Failed to stop broadcast: $error');
}
}
Future<void> _toggleFlash() async {
if (_cameraController == null) return;
try {
if (_isFlashOn) {
await _cameraController!.setFlashMode(FlashMode.off);
} else {
await _cameraController!.setFlashMode(FlashMode.torch);
}
setState(() {
_isFlashOn = !_isFlashOn;
});
} catch (error) {
print('Error toggling flash: $error');
}
}
Future<void> _changeResolution(ResolutionPreset newResolution) async {
if (_cameraController == null || _isRecording) return;
final currentCamera = _cameraController!.description;
await _cameraController!.dispose();
_currentResolution = newResolution;
await _setupCamera(currentCamera);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = twoDigits(duration.inHours);
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$hours:$minutes:$seconds';
}
Widget _buildStatsOverlay() {
return Positioned(
top: 16,
left: 16,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
AnimatedBuilder(
animation: _recordingAnimationController!,
builder: (context, child) {
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _isRecording
? Colors.red.withOpacity(_recordingAnimationController!.value)
: Colors.grey,
shape: BoxShape.circle,
),
);
},
),
SizedBox(width: 8),
Text(
_isRecording ? 'LIVE' : 'OFFLINE',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
if (_isRecording) ...[
SizedBox(height: 4),
Text(
'Duration: ${_formatDuration(_broadcastDuration)}',
style: TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Viewers: $_viewerCount',
style: TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Quality: ${_currentResolution.toString().split('.').last}',
style: TextStyle(color: Colors.white, fontSize: 12),
),
],
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Advanced Broadcast'),
backgroundColor: Colors.red,
actions: [
if (_isCameraInitialized && !_isRecording)
PopupMenuButton<ResolutionPreset>(
onSelected: _changeResolution,
itemBuilder: (context) => [
PopupMenuItem(
value: ResolutionPreset.low,
child: Text('Low Quality'),
),
PopupMenuItem(
value: ResolutionPreset.medium,
child: Text('Medium Quality'),
),
PopupMenuItem(
value: ResolutionPreset.high,
child: Text('High Quality'),
),
PopupMenuItem(
value: ResolutionPreset.veryHigh,
child: Text('Very High Quality'),
),
],
),
],
),
body: Stack(
children: [
// Camera Preview
if (_isCameraInitialized)
Positioned.fill(
child: CameraPreview(_cameraController!),
)
else
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Initializing advanced camera...'),
],
),
),
// Stats Overlay
_buildStatsOverlay(),
// Controls Overlay
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black87,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Flash Toggle
IconButton(
onPressed: _isCameraInitialized ? _toggleFlash : null,
icon: Icon(
_isFlashOn ? Icons.flash_on : Icons.flash_off,
color: _isFlashOn ? Colors.yellow : Colors.white,
size: 30,
),
),
// Main Broadcast Button
GestureDetector(
onTap: _isCameraInitialized
? (_isRecording ? _stopAdvancedBroadcast : _startAdvancedBroadcast)
: null,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: _isRecording ? Colors.red : Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
child: Icon(
_isRecording ? Icons.stop : Icons.videocam,
color: Colors.white,
size: 40,
),
),
),
// Switch Camera
IconButton(
onPressed: _isCameraInitialized && _cameras.length > 1
? _switchCamera
: null,
icon: Icon(
Icons.switch_camera,
color: Colors.white,
size: 30,
),
),
],
),
],
),
),
),
],
),
);
}
@override
void dispose() {
_recordingAnimationController?.dispose();
_broadcastTimer?.cancel();
_cameraController?.dispose();
super.dispose();
}
}
Video Playback Implementation
Video Player Screen
- Basic Player
- Advanced Player
// 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;
}
}
}
// lib/screens/advanced_video_player_screen.dart
import 'package:flutter/material.dart';
import 'package:amity_video_player/amity_video_player.dart';
import '../services/stream_service.dart';
class AdvancedVideoPlayerScreen extends StatefulWidget {
final String streamId;
const AdvancedVideoPlayerScreen({Key? key, required this.streamId}) : super(key: key);
@override
_AdvancedVideoPlayerScreenState createState() => _AdvancedVideoPlayerScreenState();
}
class _AdvancedVideoPlayerScreenState extends State<AdvancedVideoPlayerScreen>
with TickerProviderStateMixin {
final StreamService _streamService = StreamService();
AmityVideoController? _videoController;
AmityStream? _stream;
bool _isLoading = true;
bool _hasError = false;
bool _isPlaying = false;
bool _showControls = true;
// Advanced controls
double _volume = 1.0;
double _playbackSpeed = 1.0;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
AnimationController? _controlsAnimationController;
Timer? _hideControlsTimer;
@override
void initState() {
super.initState();
_controlsAnimationController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_controlsAnimationController!.forward();
_loadStream();
_startHideControlsTimer();
}
void _startHideControlsTimer() {
_hideControlsTimer?.cancel();
_hideControlsTimer = Timer(Duration(seconds: 3), () {
if (_isPlaying && _showControls) {
setState(() {
_showControls = false;
});
_controlsAnimationController!.reverse();
}
});
}
void _showControlsTemporarily() {
if (!_showControls) {
setState(() {
_showControls = true;
});
_controlsAnimationController!.forward();
}
_startHideControlsTimer();
}
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) {
_initializeAdvancedVideoPlayer(amityStream);
}
}
});
} catch (error) {
setState(() {
_isLoading = false;
_hasError = true;
});
}
}
void _initializeAdvancedVideoPlayer(AmityStream stream) {
_videoController = AmityVideoController(
streamId: stream.streamId!,
autoPlay: false,
showControls: false, // We'll use custom controls
onPlayerStateChanged: (state) {
setState(() {
_isPlaying = state == AmityVideoPlayerState.playing;
});
},
onPositionChanged: (position) {
setState(() {
_currentPosition = position;
});
},
onDurationChanged: (duration) {
setState(() {
_totalDuration = duration;
});
},
onVolumeChanged: (volume) {
setState(() {
_volume = volume;
});
},
onError: (error) {
setState(() {
_hasError = true;
});
},
);
}
Future<void> _togglePlayPause() async {
if (_videoController == null) return;
if (_isPlaying) {
await _videoController!.pause();
} else {
await _videoController!.play();
}
}
Future<void> _seekTo(Duration position) async {
if (_videoController != null) {
await _videoController!.seekTo(position);
}
}
Future<void> _setVolume(double volume) async {
if (_videoController != null) {
await _videoController!.setVolume(volume);
}
}
Future<void> _setPlaybackSpeed(double speed) async {
if (_videoController != null) {
await _videoController!.setPlaybackSpeed(speed);
setState(() {
_playbackSpeed = speed;
});
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
Widget _buildAdvancedControls() {
return AnimatedBuilder(
animation: _controlsAnimationController!,
builder: (context, child) {
return Opacity(
opacity: _controlsAnimationController!.value,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black54,
Colors.transparent,
Colors.transparent,
Colors.black87,
],
),
),
child: Column(
children: [
// Top controls
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.arrow_back, color: Colors.white),
),
Expanded(
child: Text(
_stream?.title ?? 'Video Player',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
PopupMenuButton<double>(
icon: Icon(Icons.speed, color: Colors.white),
onSelected: _setPlaybackSpeed,
itemBuilder: (context) => [
PopupMenuItem(value: 0.5, child: Text('0.5x')),
PopupMenuItem(value: 0.75, child: Text('0.75x')),
PopupMenuItem(value: 1.0, child: Text('1x')),
PopupMenuItem(value: 1.25, child: Text('1.25x')),
PopupMenuItem(value: 1.5, child: Text('1.5x')),
PopupMenuItem(value: 2.0, child: Text('2x')),
],
),
],
),
),
Spacer(),
// Bottom controls
Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Progress bar
Row(
children: [
Text(
_formatDuration(_currentPosition),
style: TextStyle(color: Colors.white, fontSize: 12),
),
Expanded(
child: Slider(
value: _totalDuration.inMilliseconds > 0
? _currentPosition.inMilliseconds / _totalDuration.inMilliseconds
: 0.0,
onChanged: (value) {
final position = Duration(
milliseconds: (_totalDuration.inMilliseconds * value).round(),
);
_seekTo(position);
},
activeColor: Colors.red,
inactiveColor: Colors.white24,
),
),
Text(
_formatDuration(_totalDuration),
style: TextStyle(color: Colors.white, fontSize: 12),
),
],
),
SizedBox(height: 8),
// Playback controls
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Volume control
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.volume_up, color: Colors.white, size: 20),
SizedBox(
width: 100,
child: Slider(
value: _volume,
onChanged: _setVolume,
activeColor: Colors.white,
inactiveColor: Colors.white24,
),
),
],
),
// Play/Pause button
GestureDetector(
onTap: _togglePlayPause,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white24,
shape: BoxShape.circle,
),
child: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 32,
),
),
),
// Speed indicator
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${_playbackSpeed}x',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: _buildBody(),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Loading stream...',
style: TextStyle(color: Colors.white),
),
],
),
);
}
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(color: Colors.white, fontSize: 18),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_isLoading = true;
_hasError = false;
});
_loadStream();
},
child: Text('Retry'),
),
],
),
);
}
return GestureDetector(
onTap: _showControlsTemporarily,
child: Stack(
children: [
// Video Player
if (_videoController != null)
Positioned.fill(
child: AmityVideoPlayer(controller: _videoController!),
),
// Advanced Controls Overlay
if (_showControls)
Positioned.fill(
child: _buildAdvancedControls(),
),
],
),
);
}
@override
void dispose() {
_controlsAnimationController?.dispose();
_hideControlsTimer?.cancel();
_videoController?.dispose();
super.dispose();
}
}
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
- Camera initialization fails: Check permissions in AndroidManifest.xml and Info.plist
- Video playback issues: Ensure proper network configuration and stream availability
- Build failures: Verify Flutter and Dart versions, run
flutter cleanandflutter pub get - iOS deployment issues: Update Podfile and run
pod install
Next Steps
- Explore Core Concepts for advanced SDK usage
- Learn about Broadcasting Features
- Implement Real-time Notifications
- Review API Reference for complete SDK documentation