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());
}
1 回复

更多关于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'),
                  ),
          ),
        ],
      ),
    );
  }
}
回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!