Flutter SDK
seenn_flutter — Reactive streams, Live Activity (iOS), Ongoing Notification (Android), ETA countdown
#Installation
flutter pub add seenn_flutter
Or add to your pubspec.yaml:
dependencies:
seenn_flutter: ^0.2.0
#Initialization
Initialize the SDK once at app startup. Must be called before using any other SDK features.
import 'package:seenn_flutter/seenn_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Seenn SDK
await Seenn.init(
appId: 'app_xxx',
userToken: userToken, // From your backend
);
runApp(const MyApp());
}
#Configuration Options
await Seenn.init(
appId: 'app_xxx',
userToken: userToken,
config: SeennConfig(
apiUrl: 'https://api.seenn.io',
sseUrl: 'https://sse.seenn.io',
timeout: Duration(seconds: 30),
debug: true,
),
);
// Or use development config for local testing
await Seenn.init(
appId: 'test_app',
userToken: 'test_token',
config: SeennConfig.development(
apiUrl: 'http://localhost:3001',
sseUrl: 'http://localhost:3000',
),
);
#jobs.subscribe()
Subscribe to updates for a specific job. Returns a JobTracker with convenient streams.
final tracker = Seenn.instance.jobs.subscribe('job_123');
// Listen to all updates
tracker.onUpdate.listen((job) {
print('Job updated: \${job.status}');
});
// Listen to progress changes only
tracker.onProgress.listen((update) {
print('Progress: \${update.progress}%');
print('Message: \${update.message}');
if (update.stage != null) {
print('Stage: \${update.stage.current}/\${update.stage.total}');
}
});
// Listen to completion
tracker.onComplete.listen((job) {
print('Completed! URL: \${job.resultUrl}');
});
// Listen to failures
tracker.onFailed.listen((job) {
print('Failed: \${job.errorMessage}');
});
// Listen to terminal state (completed or failed)
tracker.onTerminal.listen((job) {
print('Job finished with status: \${job.status}');
});
#JobTracker
The JobTracker class provides multiple streams and properties for tracking a job:
| Property / Method | Type | Description |
|---|---|---|
onUpdate |
Stream<SeennJob> |
All job updates |
onProgress |
Stream<ProgressUpdate> |
Progress changes only (deduplicated) |
onChildProgress |
Stream<ChildProgressUpdate> |
Child job progress (for parent jobs) |
onComplete |
Stream<SeennJob> |
Emits when job completes |
onFailed |
Stream<SeennJob> |
Emits when job fails |
onTerminal |
Stream<SeennJob> |
Emits on completion or failure |
current |
SeennJob? |
Current job state (sync) |
isCompleted |
bool |
Whether job is completed |
isFailed |
bool |
Whether job has failed |
isTerminal |
bool |
Whether job is in terminal state |
#Using with StreamBuilder
The SDK integrates naturally with Flutter's StreamBuilder:
class JobProgressWidget extends StatelessWidget {
final String jobId;
const JobProgressWidget({required this.jobId});
@override
Widget build(BuildContext context) {
return StreamBuilder<SeennJob?>(
stream: Seenn.instance.jobs.stream(jobId),
builder: (context, snapshot) {
final job = snapshot.data;
if (job == null) {
return const CircularProgressIndicator();
}
return Column(
children: [
Text(job.title),
LinearProgressIndicator(value: job.progress / 100),
Text('\${job.progress}%'),
if (job.message != null)
Text(job.message!),
if (job.status == JobStatus.completed)
ElevatedButton(
onPressed: () => openUrl(job.resultUrl!),
child: const Text('Download'),
),
],
);
},
);
}
}
#jobs.all$
Stream of all jobs for the current user. Updates whenever any job changes.
StreamBuilder<Map<String, SeennJob>>(
stream: Seenn.instance.jobs.all$,
builder: (context, snapshot) {
final jobs = snapshot.data?.values.toList() ?? [];
return ListView.builder(
itemCount: jobs.length,
itemBuilder: (context, index) {
final job = jobs[index];
return ListTile(
title: Text(job.title),
subtitle: Text('\${job.progress}% - \${job.status}'),
);
},
);
},
);
#Polling Mode (Self-Hosted)
For self-hosted backends without SSE infrastructure, use polling mode:
await Seenn.init(
appId: 'app_xxx',
userToken: 'user_token_xxx',
config: SeennConfig.selfHosted(
apiUrl: 'https://my-backend.com',
pollInterval: Duration(seconds: 5), // default: 5s
),
);
// Subscribe to specific jobs (required in polling mode)
Seenn.instance.subscribeJob('job_123');
Seenn.instance.subscribeJobs(['job_456', 'job_789']);
// Jobs auto-unsubscribe when they reach terminal state
// Manual unsubscribe:
Seenn.instance.unsubscribeJob('job_123');
// Check connection mode
if (Seenn.instance.isPollingMode) {
print('Using polling mode');
}
#Connection State
Monitor the SSE connection status:
// Check if connected (sync)
if (Seenn.instance.isConnected) {
print('Connected to Seenn');
}
// Stream of connection state changes
StreamBuilder<SeennConnectionState>(
stream: Seenn.instance.connectionState$,
builder: (context, snapshot) {
final state = snapshot.data ?? SeennConnectionState.disconnected;
return Icon(
state.isConnected ? Icons.cloud_done : Icons.cloud_off,
color: state.isConnected ? Colors.green : Colors.red,
);
},
);
#Connection States
| State | Description |
|---|---|
connected |
SSE connection is active and receiving events |
connecting |
Attempting to establish connection |
reconnecting |
Reconnecting after connection loss |
disconnected |
Not connected (initial state or after dispose) |
#SeennJob Model
The job model contains all job data:
| Property | Type | Description |
|---|---|---|
jobId |
String |
Unique job identifier (ULID) |
userId |
String |
User who owns this job |
status |
JobStatus |
pending | queued | running | completed | failed | cancelled |
title |
String |
Human-readable title |
jobType |
String |
Job type for categorization |
workflowId |
String? |
Workflow ID for ETA tracking |
progress |
int |
Progress percentage (0-100) |
message |
String? |
Current status message |
stage |
StageInfo? |
Current stage (name, current, total) |
queue |
QueueInfo? |
Queue position info |
estimatedCompletionAt |
String? |
ISO 8601 estimated completion time |
etaConfidence |
double? |
ETA confidence score (0.0 - 1.0) |
etaBasedOn |
int? |
Historical jobs used for ETA |
result |
JobResult? |
Result data (type, url, data) |
error |
JobError? |
Error details (code, message) |
parent |
ParentInfo? |
Parent job info (if child) |
children |
ChildrenStats? |
Children stats (if parent) |
resultUrl |
String? |
Helper: result.url |
errorMessage |
String? |
Helper: error.message |
etaRemaining |
int? |
Helper: remaining ms |
etaFormatted |
String? |
Helper: "2m 30s" |
isTerminal |
bool |
Helper: completed/failed/cancelled |
isParent |
bool |
Helper: has children |
isChild |
bool |
Helper: has parent |
#iOS Live Activity
Display job progress on the Lock Screen and Dynamic Island (iOS 16.1+).
#Setup
Add to your ios/Runner/Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>
#Usage
// Start Live Activity
final result = await Seenn.instance.liveActivity.startActivity(
jobId: 'job_123',
title: 'Processing video...',
jobType: 'video_render',
);
if (result.success) {
print('Activity started: \${result.activityId}');
}
// Update Live Activity
await Seenn.instance.liveActivity.updateActivity(
jobId: 'job_123',
progress: 50,
status: 'running',
message: 'Encoding frames...',
);
// End Live Activity
await Seenn.instance.liveActivity.endActivity(
jobId: 'job_123',
finalProgress: 100,
finalStatus: 'completed',
message: 'Your video is ready!',
);
#Auto-Sync with Job
// Start and auto-track Live Activity
final job = Seenn.instance.jobs.get('job_123');
await Seenn.instance.startLiveActivityForJob(job!);
// The SDK will automatically update/end the Live Activity
// when job state changes via SSE
#Android Ongoing Notification
Display persistent progress notification in the notification drawer (Android 5.0+).
final notificationService = OngoingNotificationService();
// Start notification
final result = await notificationService.startNotification(
jobId: 'job_123',
title: 'Processing',
jobType: 'video_render',
initialMessage: 'Starting...',
);
// Update progress
await notificationService.updateNotification(
jobId: 'job_123',
progress: 50,
status: 'running',
message: 'Halfway there...',
);
// End notification
await notificationService.endNotification(
jobId: 'job_123',
finalProgress: 100,
finalStatus: 'completed',
message: 'Your video is ready!',
);
#Cross-Platform Notifications
Use JobNotificationService for unified iOS + Android handling:
final jobNotification = JobNotificationService();
// Automatically uses Live Activity on iOS, Ongoing Notification on Android
await jobNotification.start(
jobId: 'job_123',
title: 'Processing',
jobType: 'video_render',
);
// Sync with job updates
tracker.onUpdate.listen((job) async {
await jobNotification.syncWithJob(job);
});
#ETA Countdown
Real-time countdown with server sync and confidence scores.
import 'package:seenn_flutter/seenn_flutter.dart';
// Using convenience function
final countdown = etaCountdownStream(job);
countdown.listen((state) {
print('Remaining: \${state.formatted}'); // "2m 30s"
print('Past due: \${state.isPastDue}'); // false
print('Confidence: \${state.confidence}'); // 0.85
print('Based on: \${state.basedOn} jobs'); // 42
});
#EtaCountdownService
For more control, use the service directly:
final etaService = EtaCountdownService();
// Start countdown
etaService.startCountdown(job);
// Listen to updates
etaService.stream.listen((state) {
print('ETA: \${state.formatted}');
});
// Update when job changes from server
tracker.onUpdate.listen((job) {
etaService.updateJob(job);
});
// Clean up
etaService.dispose();
#EtaCountdownState
| Property | Type | Description |
|---|---|---|
remaining |
int |
Remaining time in milliseconds |
formatted |
String |
Human-readable format ("2m 30s") |
isPastDue |
bool |
True if past estimated time |
confidence |
double? |
ETA confidence (0.0 - 1.0) |
basedOn |
int? |
Number of historical jobs used |
hasEta |
bool |
Whether ETA is available |
#Parent-Child Jobs
Track hierarchical job relationships for batch processing.
// Get parent jobs
final parents = Seenn.instance.jobs.parents;
// Get children of a specific parent
final children = Seenn.instance.jobs.childrenOf('parent_job_id');
// Stream of parent jobs
Seenn.instance.jobs.parents$.listen((parents) {
print('Parent jobs: \${parents.length}');
});
// Track child progress on a parent job
final tracker = Seenn.instance.jobs.subscribe('parent_job_id');
tracker.onChildProgress.listen((update) {
print('Children: \${update.completed}/\${update.total}');
print('Failed: \${update.failed}');
print('Running: \${update.running}');
print('Percent: \${update.percentComplete}%');
});
#Parent-Child Models
| Model | Properties | Description |
|---|---|---|
ParentInfo |
parentJobId, childIndex |
Info for child jobs |
ChildrenStats |
total, completed, failed, running, pending |
Aggregate stats for parent jobs |
ChildProgressUpdate |
parentProgress, percentComplete, allDone |
Progress stream data |
#Cleanup
// Call when user logs out or app closes
await Seenn.dispose();
// Update user token (e.g., after token refresh)
await Seenn.setUserToken(newToken);
// Force reconnect
await Seenn.instance.reconnect();