scrcpy_video_view 0.0.1
scrcpy_video_view: ^0.0.1 copied to clipboard
Low-latency Android screen streaming into Flutter textures using scrcpy and VideoToolbox on macOS.
import 'package:flutter/material.dart';
import 'package:scrcpy_video_view/scrcpy_video_view.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key, this.discoverOnStart = true});
final bool discoverOnStart;
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
final controller = ScrcpyVideoController();
List<ScrcpyDevice> devices = const [];
String? serial;
Object? discoveryError;
bool discovering = false;
@override
void initState() {
super.initState();
controller.addListener(_changed);
if (widget.discoverOnStart) _discover();
}
void _changed() {
if (mounted) setState(() {});
}
Future<void> _discover() async {
if (discovering) return;
setState(() {
discovering = true;
discoveryError = null;
});
try {
devices = await controller.discoverDevices();
if (!devices.any((device) => device.serial == serial)) {
serial = devices.length == 1 ? devices.single.serial : null;
}
} catch (error) {
discoveryError = error;
}
if (mounted) setState(() => discovering = false);
}
Future<void> _start() async {
try {
await controller.start(serial!);
} catch (_) {
// The controller exposes the failure and notifies the UI.
}
}
String _messageFor(Object error) {
if (error is! ScrcpyVideoException) return '$error';
return switch (error.code) {
ScrcpyVideoErrorCode.adbNotFound =>
'adb is not installed. Install Android platform tools or configure '
'adbPath.',
ScrcpyVideoErrorCode.scrcpyNotFound =>
'scrcpy is not installed. Install it or configure scrcpyPath.',
ScrcpyVideoErrorCode.noAuthorizedDevices =>
'Connect an Android device, enable USB debugging, and authorize this '
'Mac.',
ScrcpyVideoErrorCode.unsupportedScrcpyProtocol =>
'This scrcpy version uses an incompatible video protocol. Please '
'report the version and logs on GitHub.',
_ => error.message,
};
}
@override
void dispose() {
controller.removeListener(_changed);
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final canStop = controller.state == ScrcpyVideoState.starting ||
controller.state == ScrcpyVideoState.streaming;
final transitioning = controller.state == ScrcpyVideoState.stopping;
return MaterialApp(
title: 'example',
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.dark,
home: Scaffold(
appBar: AppBar(title: const Text('example')),
body: Row(
children: [
SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
initialValue: serial,
isExpanded: true,
hint: const Text('Select an adb device'),
items: devices
.map(
(device) => DropdownMenuItem(
value: device.serial,
child: Text(device.displayName),
),
)
.toList(),
onChanged: canStop || transitioning
? null
: (value) => setState(() => serial = value),
),
),
IconButton(
tooltip: 'Refresh devices',
onPressed: canStop || transitioning || discovering
? null
: _discover,
icon: discovering
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.refresh),
),
],
),
const SizedBox(height: 12),
FilledButton(
onPressed: transitioning || (!canStop && serial == null)
? null
: canStop
? controller.stop
: _start,
child: Text(canStop ? 'Stop' : 'Start'),
),
if (discoveryError != null || controller.error != null) ...[
const SizedBox(height: 12),
Text(_messageFor(controller.error ?? discoveryError!)),
],
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
child: Text(controller.logs.join('\n')),
),
),
],
),
),
),
Expanded(
child: ScrcpyVideo(
controller: controller,
placeholder: const Center(child: Text('No video')),
),
),
],
),
),
);
}
}