Flutter Complete Starter App
This is a complete Flutter starter for mediasfu_sdk@2.3.0. It lets a person choose a call, host dashboard, guest queue, classroom, or remote-podcast room before opening it. The room widgets supply the MediaSFU interface; your application supplies signed-in authority through two HTTPS endpoints.
Before you start
Install Flutter 3.38.1 or newer, then create a standard Flutter app named my_mediasfu_app. The starter uses the app session token that your own sign-in system issues. It never places a reusable MediaSFU credential in the application.
Your backend must accept authenticated POST requests at these routes:
/api/mediasfu/create-room/api/mediasfu/join-room
Each route receives the MediaSFU room payload unchanged and returns the package's normal create-or-join response. Apply room and role policy on that backend.
Complete example
Replace these four files in the new app.
name: my_mediasfu_app
description: A MediaSFU Flutter starter application.
publish_to: none
environment:
sdk: ">=3.10.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
http: 1.6.0
mediasfu_sdk: 2.3.0
flutter:
uses-material-design: true
enum TutorialScenario { compactCall, hostDashboard, liveGuestQueue, classroomWorkshop, remotePodcast }
const scenarioLabels = <TutorialScenario, String>{
TutorialScenario.compactCall: 'Compact call',
TutorialScenario.hostDashboard: 'Host dashboard',
TutorialScenario.liveGuestQueue: 'Live guest queue',
TutorialScenario.classroomWorkshop: 'Classroom workshop',
TutorialScenario.remotePodcast: 'Remote podcast',
};
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:mediasfu_sdk/methods/utils/create_join_room.dart';
import 'package:mediasfu_sdk/methods/utils/create_room_on_media_sfu.dart';
import 'package:mediasfu_sdk/methods/utils/join_room_on_media_sfu.dart';
import 'package:mediasfu_sdk/types/types.dart';
abstract interface class RoomBackend {
Future<CreateJoinRoomResult> create(CreateMediaSFURoomOptions payload);
Future<CreateJoinRoomResult> join(JoinMediaSFURoomOptions payload);
}
class HttpRoomBackend implements RoomBackend {
HttpRoomBackend({required String backendBaseUrl, required this.appToken, http.Client? client})
: _baseUrl = _validateBaseUrl(backendBaseUrl),
_client = client ?? http.Client();
final Uri _baseUrl;
final String appToken;
final http.Client _client;
static Uri _validateBaseUrl(String value) {
final uri = Uri.parse(value);
if (uri.scheme != 'https' || uri.host.isEmpty || uri.userInfo.isNotEmpty || uri.hasQuery || uri.hasFragment) {
throw ArgumentError('Use an absolute HTTPS app-backend URL without embedded credentials.');
}
return uri;
}
Uri _endpoint(String path) => _baseUrl.resolve(path);
Future<CreateJoinRoomResult> _post(String path, Map<String, dynamic> payload) async {
if (appToken.trim().isEmpty) {
return CreateJoinRoomResult(data: CreateJoinRoomError(error: 'Sign in before creating or joining a room.'), success: false);
}
try {
final response = await _client.post(
_endpoint(path),
headers: <String, String>{'Content-Type': 'application/json', 'Authorization': 'Bearer $appToken'},
body: jsonEncode(payload),
);
final body = response.body.isEmpty ? <String, dynamic>{} : jsonDecode(response.body) as Map<String, dynamic>;
return response.statusCode >= 200 && response.statusCode < 300
? CreateJoinRoomResult(data: CreateJoinRoomResponse.fromJson(body), success: true)
: CreateJoinRoomResult(data: CreateJoinRoomError.fromJson(body), success: false);
} catch (_) {
return CreateJoinRoomResult(data: CreateJoinRoomError(error: 'The room service is unavailable. Please try again.'), success: false);
}
}
@override
Future<CreateJoinRoomResult> create(CreateMediaSFURoomOptions payload) => _post('/api/mediasfu/create-room', payload.toMap());
@override
Future<CreateJoinRoomResult> join(JoinMediaSFURoomOptions payload) => _post('/api/mediasfu/join-room', payload.toMap());
}
class SecureRoomGateway {
SecureRoomGateway(this.backend);
final RoomBackend backend;
Future<CreateJoinRoomResult> create(CreateMediaSFUOptions request) => backend.create(request.payload);
Future<CreateJoinRoomResult> join(JoinMediaSFUOptions request) => backend.join(request.payload);
}
import 'package:flutter/material.dart';
import 'package:mediasfu_sdk/components/mediasfu_components/mediasfu_conference.dart';
import 'package:mediasfu_sdk/components/mediasfu_components/mediasfu_generic.dart';
import 'package:mediasfu_sdk/components/mediasfu_components/mediasfu_webinar.dart';
import 'package:mediasfu_sdk/methods/utils/mediasfu_parameters.dart';
import 'room_backend.dart';
import 'scenarios.dart';
void main() => runApp(const TutorialApp());
class TutorialApp extends StatefulWidget {
const TutorialApp({super.key});
@override
State<TutorialApp> createState() => _TutorialAppState();
}
class _TutorialAppState extends State<TutorialApp> {
TutorialScenario scenario = TutorialScenario.compactCall;
final backendController = TextEditingController();
final tokenController = TextEditingController();
HttpRoomBackend? backend;
@override
void dispose() {
backendController.dispose();
tokenController.dispose();
super.dispose();
}
void connect() {
try {
setState(() => backend = HttpRoomBackend(backendBaseUrl: backendController.text.trim(), appToken: tokenController.text.trim()));
} on ArgumentError {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter an absolute HTTPS address for your signed-in app backend.')));
}
}
@override
Widget build(BuildContext context) {
final currentBackend = backend;
if (currentBackend != null) return _ScenarioRoom(scenario: scenario, backend: currentBackend);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('MediaSFU Flutter starter')),
body: Padding(
padding: const EdgeInsets.all(20),
child: ListView(
children: <Widget>[
const Text('Choose an outcome, then enter the HTTPS address of your signed-in app backend and a short-lived app session token. These are not MediaSFU Cloud credentials.'),
const SizedBox(height: 16),
DropdownButton<TutorialScenario>(
value: scenario,
isExpanded: true,
items: TutorialScenario.values.map((value) => DropdownMenuItem(value: value, child: Text(scenarioLabels[value]!))).toList(),
onChanged: (value) => setState(() => scenario = value!),
),
TextField(controller: backendController, keyboardType: TextInputType.url, decoration: const InputDecoration(labelText: 'HTTPS app-backend URL')),
TextField(controller: tokenController, obscureText: true, autocorrect: false, decoration: const InputDecoration(labelText: 'Short-lived app session token')),
const SizedBox(height: 16),
ElevatedButton(onPressed: connect, child: const Text('Open room')),
const SizedBox(height: 12),
const Text('Replace this temporary app-token screen with your normal signed-in session before release. Do not put a MediaSFU API key in the mobile app.'),
],
),
),
),
);
}
}
class _ScenarioRoom extends StatefulWidget {
const _ScenarioRoom({required this.scenario, required this.backend});
final TutorialScenario scenario;
final HttpRoomBackend backend;
@override
State<_ScenarioRoom> createState() => _ScenarioRoomState();
}
class _ScenarioRoomState extends State<_ScenarioRoom> {
MediasfuParameters? parameters;
@override
Widget build(BuildContext context) {
final gateway = SecureRoomGateway(widget.backend);
if (widget.scenario == TutorialScenario.liveGuestQueue) {
return MaterialApp(home: MediasfuWebinar(options: MediasfuWebinarOptions(connectMediaSFU: true, returnUI: true, sourceParameters: parameters, updateSourceParameters: (next) => setState(() => parameters = next), createMediaSFURoom: gateway.create, joinMediaSFURoom: gateway.join)));
}
if (widget.scenario == TutorialScenario.compactCall) {
return MaterialApp(home: MediasfuGeneric(options: MediasfuGenericOptions(connectMediaSFU: true, returnUI: true, sourceParameters: parameters, updateSourceParameters: (next) => setState(() => parameters = next), createMediaSFURoom: gateway.create, joinMediaSFURoom: gateway.join)));
}
return MaterialApp(home: MediasfuConference(options: MediasfuConferenceOptions(connectMediaSFU: true, returnUI: true, sourceParameters: parameters, updateSourceParameters: (next) => setState(() => parameters = next), createMediaSFURoom: gateway.create, joinMediaSFURoom: gateway.join)));
}
}
Run
flutter create my_mediasfu_app
cd my_mediasfu_app
flutter pub get
flutter run
Choose the outcome on the first screen, enter your HTTPS backend URL and a short-lived app session token, then use the MediaSFU room screen to create or join.
What you should see
The selected room opens after the backend authorizes it. Compact call uses the supplied call room, live guest queue uses the supplied webinar room, and the other choices use the supplied conference room.
Release checklist
- Add camera and microphone permission descriptions to the Android and iOS projects produced by Flutter.
- Replace the temporary token field with the app's ordinary sign-in session.
- Verify create, join, media permission recovery, another participant's audio, leave, and authorized room end on physical devices.