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

1 回复

更多关于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),
      ),
    );
  }
}

注意事项

  1. 权限处理:确保在AndroidManifest.xmlInfo.plist中添加了必要的摄像头和麦克风权限。

  2. 错误处理:在实际应用中,需要更完善的错误处理机制,比如处理摄像头不可用、权限被拒绝等情况。

  3. 插件版本:由于插件API可能会随时间变化,请参考video_stream插件的官方文档获取最新的使用方法和API。

  4. 纹理ID:在Flutter中,视频流通常通过纹理(Texture)来显示,确保插件提供了获取纹理ID的方法。

  5. 释放资源:在组件销毁时,务必释放视频流控制器占用的资源,以避免内存泄漏。

以上代码提供了一个基本的框架,用于在Flutter应用中处理视频流。根据实际需求,你可能需要调整代码,以适应特定的业务逻辑和UI设计。

回到顶部