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

Seenn.init({appId, userToken, config?})

Initialize the SDK once at app startup. Must be called before using any other SDK features.

Parameters
appId String Your Seenn app IDrequired
userToken String User token from your backendrequired
config SeennConfig? Optional configuration options
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()

Seenn.instance.jobs.subscribe(jobId) → JobTracker

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$

Seenn.instance.jobs.all$ → Stream<Map<String, SeennJob>>

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');
}
When to use polling: Polling mode is ideal for self-hosted backends without Redis or SSE server. For Seenn Cloud, use the default SSE 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 iOS

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 Android

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();