Flutter视频流处理插件video_stream的使用
Flutter视频流处理插件video_stream的使用
视频流处理插件video_stream
一个用于将基本实时视频流传输到RTMP服务器的新Flutter插件。
开始使用
此插件是camera_with_rtmp
的一个简化版本,它是flutter
相机插件的扩展,增加了RTMP流媒体功能。它适用于Android和iOS(但不支持Web)。
功能
- 在小部件中显示实时摄像头预览。
- 将视频流传输到RTMP服务器。
iOS配置
在ios/Runner/Info.plist
文件中添加两行:
- 一个带有键
Privacy - Camera Usage Description
和描述。 - 另一个带有键
Privacy - Microphone Usage Description
和描述。
或者以文本格式添加以下内容:
<key>NSCameraUsageDescription</key>
<string>可以使用摄像头吗?</string>
<key>NSMicrophoneUsageDescription</key>
<string>可以使用麦克风吗?</string>
Android配置
在你的android/app/build.gradle
文件中,将最低Android SDK版本改为21或更高:
minSdkVersion 21
还需要添加一个打包选项来排除一个文件,否则Gradle在构建时会报错:
packagingOptions {
exclude 'project.clj'
}
完整示例
以下是使用video_stream
插件的完整示例代码:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:video_stream/camera.dart';
import 'package:wakelock/wakelock.dart';
class CameraExampleHome extends StatefulWidget {
const CameraExampleHome({Key? key}) : super(key: key);
[@override](/user/override)
_CameraExampleHomeState createState() {
return _CameraExampleHomeState();
}
}
/// 返回适合给定方向的相机图标。
IconData getCameraLensIcon(CameraLensDirection direction) {
switch (direction) {
case CameraLensDirection.back:
return Icons.camera_rear;
case CameraLensDirection.front:
return Icons.camera_front;
case CameraLensDirection.external:
return Icons.camera;
}
}
void logError(String code, String message) =>
print('Error: $code\nError Message: $message');
class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController? controller =
CameraController(cameras[1], ResolutionPreset.high);
String? imagePath;
String? videoPath;
String? url;
VideoPlayerController? videoController;
late VoidCallback videoPlayerListener;
bool enableAudio = true;
bool useOpenGL = true;
String streamURL = "rtmp://[your rtmp server address]/live";
bool streaming = false;
String? cameraDirection;
Timer? _timer;
[@override](/user/override)
void initState() {
_initialize();
super.initState();
WidgetsBinding.instance.addObserver(this);
}
[@override](/user/override)
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Future<void> _initialize() async {
streaming = false;
cameraDirection = 'front';
// controller = CameraController(cameras[1], Resolution.high);
await controller!.initialize();
if (!mounted) {
return;
}
setState(() {});
}
[@override](/user/override)
void didChangeAppLifecycleState(AppLifecycleState state) {
// 应用状态改变,但在初始化之前。
if (controller == null || !controller!.value.isInitialized) {
return;
}
if (state == AppLifecycleState.inactive) {
controller?.dispose();
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
} else if (state == AppLifecycleState.resumed) {
if (controller != null) {
onNewCameraSelected(controller!.description!);
}
}
}
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
toggleCameraDirection() async {
if (cameraDirection == 'front') {
if (controller != null) {
await controller?.dispose();
}
controller = CameraController(
cameras[0],
ResolutionPreset.high,
enableAudio: enableAudio,
androidUseOpenGL: useOpenGL,
);
// 如果控制器更新了,则更新UI。
controller!.addListener(() {
if (mounted) setState(() {});
if (controller!.value.hasError) {
showInSnackBar('Camera error ${controller!.value.errorDescription}');
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
Wakelock.disable();
}
});
try {
await controller!.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
cameraDirection = 'back';
} else {
if (controller != null) {
await controller!.dispose();
}
controller = CameraController(
cameras[1],
ResolutionPreset.high,
enableAudio: enableAudio,
androidUseOpenGL: useOpenGL,
);
// 如果控制器更新了,则更新UI。
controller!.addListener(() {
if (mounted) setState(() {});
if (controller!.value.hasError) {
showInSnackBar('Camera error ${controller!.value.errorDescription}');
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
Wakelock.disable();
}
});
try {
await controller!.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
cameraDirection = 'front';
}
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: true,
key: _scaffoldKey,
body: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Stack(
children: <Widget>[
Container(
color: Colors.black,
child: Center(
child: _cameraPreviewWidget(),
),
),
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
backgroundColor: Colors.transparent,
elevation: 0.0,
title: streaming
? ElevatedButton(
onPressed: () => onStopButtonPressed(),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.red)),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.videocam_off),
SizedBox(width: 10),
Text(
'结束流',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
],
),
)
: ElevatedButton(
onPressed: () => onVideoStreamingButtonPressed(),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all<Color>(Colors.blue)),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.videocam),
SizedBox(width: 10),
Text(
'开始流',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
],
),
),
actions: [
Padding(
padding: const EdgeInsets.all(10.0),
child: IconButton(
color: Theme.of(context).primaryColor,
icon: const Icon(Icons.switch_video),
tooltip: '切换摄像头',
onPressed: () {
toggleCameraDirection();
},
),
),
],
),
),
],
),
),
),
);
}
/// 显示从摄像头获取的预览(如果没有可用的预览则显示一条消息)。
Widget _cameraPreviewWidget() {
if (controller == null || !controller!.value.isInitialized) {
return const Text(
'点击选择一个摄像头',
style: TextStyle(
color: Colors.white,
fontSize: 24.0,
fontWeight: FontWeight.w900,
),
);
} else {
return AspectRatio(
aspectRatio: controller!.value.aspectRatio,
child: CameraPreview(controller!),
);
}
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
void onNewCameraSelected(CameraDescription? cameraDescription) async {
if (controller != null) {
await controller!.dispose();
}
if (cameraDescription == null) {
print('cameraDescription is null');
}
controller = CameraController(
cameraDescription,
ResolutionPreset.medium,
enableAudio: enableAudio,
androidUseOpenGL: useOpenGL,
);
// 如果控制器更新了,则更新UI。
controller!.addListener(() {
if (mounted) setState(() {});
if (controller!.value.hasError) {
showInSnackBar('Camera error ${controller!.value.errorDescription}');
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
Wakelock.disable();
}
});
try {
await controller!.initialize();
} on CameraException catch (e) {
_showCameraException(e);
}
if (mounted) {
setState(() {});
}
}
void onVideoStreamingButtonPressed() {
startVideoStreaming().then((url) {
if (mounted) {
setState(() {
streaming = true;
});
}
if (url!.isNotEmpty) showInSnackBar('正在向$url流视频');
Wakelock.enable();
});
}
void onStopButtonPressed() {
stopVideoStreaming().then((_) {
if (mounted) {
setState(() {
streaming = false;
});
}
showInSnackBar('停止流到:$url');
});
Wakelock.disable();
}
void onPauseStreamingButtonPressed() {
pauseVideoStreaming().then((_) {
if (mounted) setState(() {});
showInSnackBar('暂停流');
});
}
void onResumeStreamingButtonPressed() {
resumeVideoStreaming().then((_) {
if (mounted) setState(() {});
showInSnackBar('恢复流');
});
}
Future<String?> startVideoStreaming() async {
if (!controller!.value.isInitialized) {
showInSnackBar('错误:请先选择一个摄像头。');
return null;
}
// 打开一个对话框以输入URL
String myUrl = streamURL;
try {
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
url = myUrl;
await controller!.startVideoStreaming(url!, androidUseOpenGL: false);
// _timer = Timer.periodic(Duration(seconds: 1), (timer) async {
// var stats = await controller!.getStreamStatistics();
// print(stats);
// });
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
return url;
}
Future<void> stopVideoStreaming() async {
try {
await controller!.stopVideoStreaming();
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
} on CameraException catch (e) {
_showCameraException(e);
return;
}
}
Future<void> pauseVideoStreaming() async {
if (!controller!.value.isStreamingVideoRtmp) {
return;
}
try {
await controller!.pauseVideoStreaming();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Future<void> resumeVideoStreaming() async {
try {
await controller!.resumeVideoStreaming();
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('错误:${e.code}\n${e.description}');
}
}
class CameraApp extends StatelessWidget {
const CameraApp({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return const MaterialApp(
home: CameraExampleHome(),
);
}
}
List<CameraDescription> cameras = [];
Future<void> main() async {
// 在初始化应用之前获取可用的摄像头。
try {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras();
} on CameraException catch (e) {
logError(e.code, e.description);
}
runApp(const CameraApp());
}
更多关于Flutter视频流处理插件video_stream的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter视频流处理插件video_stream的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用video_stream
插件来处理视频流的示例代码。请注意,这个插件的具体名称和功能可能会因版本和时间变化而有所不同,因此以下示例基于一个假设的插件API。如果实际插件的API不同,请根据文档进行调整。
首先,确保你已经在pubspec.yaml
文件中添加了video_stream
插件的依赖:
dependencies:
flutter:
sdk: flutter
video_stream: ^x.y.z # 替换为实际的版本号
然后,运行flutter pub get
来安装依赖。
接下来,我们编写一个示例应用,展示如何使用video_stream
插件来获取并显示视频流。
主应用代码 (main.dart)
import 'package:flutter/material.dart';
import 'package:video_stream/video_stream.dart'; // 假设插件的导入路径
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: VideoStreamPage(),
);
}
}
class VideoStreamPage extends StatefulWidget {
@override
_VideoStreamPageState createState() => _VideoStreamPageState();
}
class _VideoStreamPageState extends State<VideoStreamPage> {
late VideoStreamController _controller;
@override
void initState() {
super.initState();
// 初始化VideoStreamController
_controller = VideoStreamController(
// 假设的初始化参数,根据实际情况调整
cameraIndex: 0, // 选择摄像头索引
resolution: Resolution.high, // 选择分辨率
);
// 开始视频流
_controller.start().then((value) {
if (value) {
print("Video stream started successfully.");
} else {
print("Failed to start video stream.");
}
}).catchError((error) {
print("Error starting video stream: $error");
});
}
@override
void dispose() {
// 释放资源
_controller.stop();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Video Stream Example'),
),
body: Center(
child: _controller.textureId != null
? Texture(id: _controller.textureId!)
: Container(
child: CircularProgressIndicator(),
width: 200,
height: 200,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 切换摄像头或执行其他操作
setState(() {
_controller.switchCamera();
});
},
tooltip: 'Switch Camera',
child: Icon(Icons.camera_switch),
),
);
}
}
注意事项
-
权限处理:确保在
AndroidManifest.xml
和Info.plist
中添加了必要的摄像头和麦克风权限。 -
错误处理:在实际应用中,需要更完善的错误处理机制,比如处理摄像头不可用、权限被拒绝等情况。
-
插件版本:由于插件API可能会随时间变化,请参考
video_stream
插件的官方文档获取最新的使用方法和API。 -
纹理ID:在Flutter中,视频流通常通过纹理(Texture)来显示,确保插件提供了获取纹理ID的方法。
-
释放资源:在组件销毁时,务必释放视频流控制器占用的资源,以避免内存泄漏。
以上代码提供了一个基本的框架,用于在Flutter应用中处理视频流。根据实际需求,你可能需要调整代码,以适应特定的业务逻辑和UI设计。