Flutter实时RTMP流媒体摄像头插件camera_with_rtmp的使用
camera_with_rtmp
是一个基于 Flutter 的摄像头插件扩展,用于实现 RTMP 实时流媒体功能。该插件支持 Android 和 iOS 平台,但不支持 Web。它继承了官方的 camera
插件,并在此基础上增加了 RTMP 流媒体功能。
功能概述
- 在小部件中显示实时摄像头预览。
- 捕获快照并保存到文件。
- 录制视频。
- 通过 Dart 访问图像流。
安装
1. 添加依赖
在 pubspec.yaml
文件中添加 camera_with_rtmp
作为依赖:
dependencies:
camera_with_rtmp: ^版本号
然后运行以下命令以更新依赖项:
flutter pub get
2. iOS 配置
在 ios/Runner/Info.plist
文件中添加以下两行配置:
<key>NSCameraUsageDescription</key>
<string>允许使用相机</string>
<key>NSMicrophoneUsageDescription</key>
<string>允许使用麦克风</string>
3. Android 配置
在 android/app/build.gradle
文件中将最小 SDK 版本设置为 21 或更高:
minSdkVersion 21
同时,排除可能导致构建错误的文件:
packagingOptions {
exclude 'project.clj'
}
示例代码
以下是一个完整的示例代码,展示了如何使用 camera_with_rtmp
插件进行 RTMP 流媒体直播。
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:camera_with_rtmp/camera.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';
import 'package:wakelock/wakelock.dart';
class CameraExampleHome extends StatefulWidget {
[@override](/user/override)
_CameraExampleHomeState createState() => _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;
}
throw ArgumentError('未知镜头方向');
}
void logError(String code, String message) =>
print('错误: $code\n错误信息: $message');
class _CameraExampleHomeState extends State<CameraExampleHome>
with WidgetsBindingObserver {
CameraController controller;
String imagePath;
String videoPath;
String url;
VideoPlayerController videoController;
VoidCallback videoPlayerListener;
bool enableAudio = true;
bool useOpenGL = true;
TextEditingController _textFieldController = TextEditingController(
text: "rtmp://34.70.40.166:1935/LiveApp/815794454132232781694481");
Timer _timer;
[@override](/user/override)
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
[@override](/user/override)
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
[@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>();
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('摄像头示例'),
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Center(
child: _cameraPreviewWidget(),
),
),
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(
color: controller != null && controller.value.isRecordingVideo
? controller.value.isStreamingVideoRtmp
? Colors.redAccent
: Colors.orangeAccent
: controller != null && controller.value.isStreamingVideoRtmp
? Colors.blueAccent
: Colors.grey,
width: 3.0,
),
),
),
),
_captureControlRowWidget(),
_toggleAudioWidget(),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_cameraTogglesRowWidget(),
_thumbnailWidget(),
],
),
),
],
),
);
}
/// 显示摄像头预览或提示信息。
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),
);
}
}
/// 切换音频开关。
Widget _toggleAudioWidget() {
return Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
children: <Widget>[
const Text('启用音频:'),
Switch(
value: enableAudio,
onChanged: (bool value) {
enableAudio = value;
if (controller != null) {
onNewCameraSelected(controller.description);
}
},
),
],
),
);
}
/// 显示捕获的图片或视频缩略图。
Widget _thumbnailWidget() {
return Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
videoController == null && imagePath == null
? Container()
: SizedBox(
child: (videoController == null)
? Image.file(File(imagePath))
: Container(
child: Center(
child: AspectRatio(
aspectRatio:
videoController.value.size != null
? videoController.value.aspectRatio
: 1.0,
child: VideoPlayer(videoController)),
),
decoration: BoxDecoration(
border: Border.all(color: Colors.pink)),
),
width: 64.0,
height: 64.0,
),
],
),
),
);
}
/// 显示控制栏,包含拍照和录制按钮。
Widget _captureControlRowWidget() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IconButton(
icon: const Icon(Icons.camera_alt),
color: Colors.blue,
onPressed: controller != null && controller.value.isInitialized
? onTakePictureButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.videocam),
color: Colors.blue,
onPressed: controller != null &&
controller.value.isInitialized &&
!controller.value.isRecordingVideo
? onVideoRecordButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.watch),
color: Colors.blue,
onPressed: controller != null &&
controller.value.isInitialized &&
!controller.value.isStreamingVideoRtmp
? onVideoStreamingButtonPressed
: null,
),
IconButton(
icon: controller != null &&
(controller.value.isRecordingPaused || controller.value.isStreamingPaused)
? Icon(Icons.play_arrow)
: Icon(Icons.pause),
color: Colors.blue,
onPressed: controller != null &&
controller.value.isInitialized &&
(controller.value.isRecordingVideo ||
controller.value.isStreamingVideoRtmp)
? (controller != null &&
(controller.value.isRecordingPaused ||
controller.value.isStreamingPaused)
? onResumeButtonPressed
: onPauseButtonPressed)
: null,
),
IconButton(
icon: const Icon(Icons.stop),
color: Colors.red,
onPressed: controller != null &&
controller.value.isInitialized &&
(controller.value.isRecordingVideo ||
controller.value.isStreamingVideoRtmp)
? onStopButtonPressed
: null,
)
],
);
}
/// 显示摄像头切换按钮。
Widget _cameraTogglesRowWidget() {
final List<Widget> toggles = [];
if (cameras.isEmpty) {
return const Text('未找到摄像头');
} else {
for (CameraDescription cameraDescription in cameras) {
toggles.add(
SizedBox(
width: 90.0,
child: RadioListTile<CameraDescription>(
title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
groupValue: controller?.description,
value: cameraDescription,
onChanged: controller != null && controller.value.isRecordingVideo
? null
: onNewCameraSelected,
),
),
);
}
}
return Row(children: toggles);
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message)));
}
void onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
}
controller = CameraController(
cameraDescription,
ResolutionPreset.medium,
enableAudio: enableAudio,
androidUseOpenGL: useOpenGL,
);
controller.addListener(() {
if (mounted) setState(() {});
if (controller.value.hasError) {
showInSnackBar('摄像头错误: ${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 onTakePictureButtonPressed() {
takePicture().then((String filePath) {
if (mounted) {
setState(() {
imagePath = filePath;
videoController?.dispose();
videoController = null;
});
if (filePath != null) showInSnackBar('照片已保存至 $filePath');
}
});
}
void onVideoRecordButtonPressed() {
startVideoRecording().then((String filePath) {
if (mounted) setState(() {});
if (filePath != null) showInSnackBar('视频已保存至 $filePath');
Wakelock.enable();
});
}
void onVideoStreamingButtonPressed() async {
String url = await _getUrl();
if (url != null) {
startVideoStreaming(url).then((_) {
if (mounted) setState(() {});
showInSnackBar('视频正在流式传输至 $url');
});
}
}
void onStopButtonPressed() {
if (this.controller.value.isStreamingVideoRtmp) {
stopVideoStreaming().then((_) {
if (mounted) setState(() {});
showInSnackBar('视频已流式传输至: $url');
});
} else {
stopVideoRecording().then((_) {
if (mounted) setState(() {});
showInSnackBar('视频已保存至: $videoPath');
});
}
Wakelock.disable();
}
Future<String> _getUrl() async {
String result = _textFieldController.text;
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('输入流媒体地址'),
content: TextField(
controller: _textFieldController,
decoration: InputDecoration(hintText: "输入流媒体地址"),
onChanged: (String str) => result = str,
),
actions: <Widget>[
FlatButton(
child: Text('取消'),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('确定'),
onPressed: () {
Navigator.pop(context, result);
},
)
],
);
});
}
Future<void> startVideoStreaming(String url) async {
if (!controller.value.isInitialized) {
showInSnackBar('错误: 请选择一个摄像头');
return;
}
if (controller.value.isStreamingVideoRtmp) {
return;
}
try {
url = await _getUrl();
if (_timer != null) {
_timer.cancel();
_timer = null;
}
await controller.startVideoStreaming(url);
_timer = Timer.periodic(Duration(seconds: 1), (timer) async {
var stats = await controller.getStreamStatistics();
print(stats);
});
} on CameraException catch (e) {
_showCameraException(e);
}
}
Future<void> stopVideoStreaming() async {
if (!controller.value.isStreamingVideoRtmp) {
return;
}
try {
await controller.stopVideoStreaming();
if (_timer != null) {
_timer.cancel();
_timer = null;
}
} on CameraException catch (e) {
_showCameraException(e);
}
}
Future<void> _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('错误: ${e.code}\n描述: ${e.description}');
}
}
class CameraApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return 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(CameraApp());
}
更多关于Flutter实时RTMP流媒体摄像头插件camera_with_rtmp的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
camera_with_rtmp
是一个 Flutter 插件,它允许你在 Flutter 应用中捕获摄像头视频并将其实时推流到 RTMP 服务器。这个插件结合了摄像头捕获和 RTMP 推流功能,非常适合需要实时视频流的应用场景,如直播、视频监控等。
安装插件
首先,你需要在 pubspec.yaml
文件中添加 camera_with_rtmp
插件的依赖:
dependencies:
flutter:
sdk: flutter
camera_with_rtmp: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get
来安装插件。
使用插件
以下是一个简单的示例,展示如何使用 camera_with_rtmp
插件来捕获摄像头视频并推流到 RTMP 服务器。
import 'package:flutter/material.dart';
import 'package:camera_with_rtmp/camera_with_rtmp.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: CameraScreen(),
);
}
}
class CameraScreen extends StatefulWidget {
[@override](/user/override)
_CameraScreenState createState() => _CameraScreenState();
}
class _CameraScreenState extends State<CameraScreen> {
CameraWithRtmpController? _controller;
bool _isStreaming = false;
[@override](/user/override)
void initState() {
super.initState();
_initializeCamera();
}
Future<void> _initializeCamera() async {
_controller = CameraWithRtmpController();
await _controller!.initialize();
setState(() {});
}
Future<void> _startStreaming() async {
if (_controller != null) {
await _controller!.startStreaming('rtmp://your-rtmp-server-url/live/stream');
setState(() {
_isStreaming = true;
});
}
}
Future<void> _stopStreaming() async {
if (_controller != null) {
await _controller!.stopStreaming();
setState(() {
_isStreaming = false;
});
}
}
[@override](/user/override)
void dispose() {
_controller?.dispose();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
if (_controller == null || !_controller!.isInitialized) {
return Center(child: CircularProgressIndicator());
}
return Scaffold(
appBar: AppBar(
title: Text('RTMP Camera Stream'),
),
body: Column(
children: [
Expanded(
child: CameraWithRtmpPreview(_controller!),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: _isStreaming
? ElevatedButton(
onPressed: _stopStreaming,
child: Text('Stop Streaming'),
)
: ElevatedButton(
onPressed: _startStreaming,
child: Text('Start Streaming'),
),
),
],
),
);
}
}