Skip to main content

rtmp_streaming 3.0.0 copy "rtmp_streaming: ^3.0.0" to clipboard
rtmp_streaming: ^3.0.0 copied to clipboard

A Flutter plugin for Camera and Microphone streaming library via RTMP.

example/lib/main.dart

// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:rtmp_streaming/rtmp_streaming.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

class CameraExampleHome extends StatefulWidget {
  const CameraExampleHome({super.key});
  @override
  CameraExampleHomeState createState() {
    return CameraExampleHomeState();
  }
}

/// Returns a suitable camera icon for [direction].
IconData getCameraLensIcon(CameraLensDirection? direction) {
  switch (direction) {
    case CameraLensDirection.back:
      return Icons.camera_rear;
    case CameraLensDirection.front:
      return Icons.camera_front;
    case CameraLensDirection.external:
    default:
      return Icons.camera;
  }
}

void logError(String code, String message) =>
    debugPrint('Error: $code\nError Message: $message');

class CameraExampleHomeState extends State<CameraExampleHome>
    with WidgetsBindingObserver {
  final CameraController controller = CameraController(
    ResolutionPreset.medium,
    enableAudio: true,
  );
  String? imagePath;
  String? videoPath;
  VoidCallback? videoPlayerListener;
  bool enableAudio = true; // 是否启用音频
  bool isFlashLight = false; // false表示关闭闪光灯,true表示打开闪光灯
  CameraDescription? _cameraDesc;
  final TextEditingController _textFieldController = TextEditingController(
    text: "rtmp://192.168.2.81/live/livestream",
  );
  late final TextEditingController _multiUrl1Controller;
  late final TextEditingController _multiUrl2Controller;
  StreamingProtocol _selectedProtocol = StreamingProtocol.rtmp;
  StreamingProtocol _multiProtocol1 = StreamingProtocol.rtmp;
  StreamingProtocol _multiProtocol2 = StreamingProtocol.srt;
  bool _isMultiStreaming = false;
  StreamingCapabilities? _capabilities;

  List<StreamingProtocol> get _availableProtocols {
    return StreamingProtocol.values
        .where(
          (p) => p != StreamingProtocol.whep && isStreamingProtocolSupported(p),
        )
        .toList(growable: false);
  }

  /// iOS multi-stream demo: RTMP + SRT.
  List<StreamingProtocol> get _multiProtocols => const [
    StreamingProtocol.rtmp,
    StreamingProtocol.srt,
  ];

  /// RootEncoder 2.7.0+:BT.709 与 RTMP ping/RTT 示例
  bool _forceBt709 = false;
  bool _rtmpShouldSendPings = false;
  Timer? _streamStatsTimer;
  String _androidStreamStatsLine = '';
  double _pitchShift = 1.0;
  bool _exposureLocked = false;

  /// HaishinKit 2.2.5+:分屏/多任务时保持相机(iOS 17+)
  bool _iosMultitaskingCamera = false;
  VideoSourceType _videoSource = VideoSourceType.cameraX;
  VideoCodecType _videoCodec = VideoCodecType.h264;
  AudioCodecType? _audioCodec;

  bool get isStreaming => controller.value.isStreaming ?? false;

  bool get isControllerInitialized => controller.value.isInitialized ?? false;
  bool get isRecordingVideo => controller.value.isRecordingVideo ?? false;
  bool get isRecordingPaused => controller.value.isRecordingPaused;
  bool get isStreamingPaused => controller.value.isStreamingPaused;
  bool get isTakingPicture => controller.value.isTakingPicture ?? false;

  @override
  void initState() {
    _multiUrl1Controller = TextEditingController(
      text: 'rtmp://192.168.2.81/live/livestream',
    );
    _multiUrl2Controller = TextEditingController(
      text: 'srt://192.168.2.81:10080?streamid=#!::r=live/livestream,m=publish',
    );
    onInit();
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    onDispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  //
  void onDispose() async {
    _streamStatsTimer?.cancel();
    _textFieldController.dispose();
    _multiUrl1Controller.dispose();
    _multiUrl2Controller.dispose();
    await WakelockPlus.disable();
    await controller.dispose();
  }
  // @override
  // Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
  //   // App state changed before we got the chance to initialize.
  //   if (!isControllerInitialized) {
  //     return;
  //   }
  //   if (state == AppLifecycleState.paused) {

  //     await pauseVideoRecording();
  //   } else if (state == AppLifecycleState.resumed) {
  //     await resumeVideoRecording();
  //   }
  // }

  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    Color color = Colors.grey;

    if (isRecordingVideo) {
      color = Colors.redAccent;
    } else if (isStreaming) {
      color = Colors.blueAccent;
    }

    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('Camera example'),
        actions: Platform.isAndroid
            ? [
                ElevatedButton(
                  onPressed: isControllerInitialized
                      ? () async {
                          await controller.setVideoEffect(
                            VideoEffect.deformation,
                          );
                        }
                      : null,
                  child: Text("set filter"),
                ),
                ElevatedButton(
                  onPressed: isControllerInitialized
                      ? () async {
                          await controller.clearVideoEffect();
                        }
                      : null,
                  child: Text("remove filter"),
                ),
                ElevatedButton(
                  onPressed: isControllerInitialized
                      ? () async {
                          await controller.setVideoEffect(VideoEffect.sobel);
                        }
                      : null,
                  child: const Text('edge HQ'),
                ),
              ]
            : null,
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            flex: 2,
            child: Container(
              decoration: BoxDecoration(
                color: Colors.black,
                border: Border.all(color: color, width: 3.0),
              ),
              child: Center(child: _cameraPreviewWidget()),
            ),
          ),
          Expanded(flex: 1, child: _captureControlRowWidget()),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: isControllerInitialized
            ? () async {
                await controller.dispose();
              }
            : null,
        child: Icon(Icons.close),
      ),
    );
  }

  /// Display camera preview (or a message if the preview is not available).
  Widget _cameraPreviewWidget() {
    if (!isControllerInitialized) {
      return const Text(
        'Tap a camera',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24.0,
          fontWeight: FontWeight.w900,
        ),
      );
    }

    return AspectRatio(
      aspectRatio: controller.value.aspectRatio,
      child: CameraPreview(controller),
    );
  }

  /// Display the thumbnail of the captured image or video.
  Widget _thumbnailWidget() {
    return Align(
      alignment: Alignment.centerRight,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          imagePath == null
              ? Container()
              : SizedBox(
                  width: 64.0,
                  height: 64.0,
                  child: Image.file(File(imagePath!)),
                ),
        ],
      ),
    );
  }

  /// Display the control bar with buttons to take pictures and record videos.
  Widget _captureControlRowWidget() {
    if (!isControllerInitialized) return Container();

    return ListView(
      children: <Widget>[
        // Only Android has implemented it
        if (Platform.isAndroid)
          ElevatedButton.icon(
            icon: const Icon(Icons.camera_alt, color: Colors.white),
            style: ElevatedButton.styleFrom(backgroundColor: Colors.blue),
            label: Text("Take Picture", style: TextStyle(color: Colors.white)),
            onPressed:
                (isControllerInitialized && (isRecordingVideo || isStreaming))
                ? onTakePictureButtonPressed
                : null,
          ),
        SizedBox(width: 5),
        // Record Localhost video
        ElevatedButton.icon(
          icon: Icon(
            !isRecordingVideo ? Icons.videocam : Icons.stop,
            color: Colors.white,
          ),
          style: ElevatedButton.styleFrom(
            backgroundColor: !isRecordingVideo ? Colors.blue : Colors.red,
          ),
          label: Text(
            !isRecordingVideo ? "Start Record" : "Stop Record",
            style: TextStyle(color: Colors.white),
          ),
          onPressed: isControllerInitialized
              ? () {
                  if (!isRecordingVideo) {
                    onVideoRecordButtonPressed();
                  } else {
                    stopVideoRecording();
                  }
                }
              : null,
        ),
        SizedBox(width: 5),
        ElevatedButton.icon(
          icon: Icon(
            !isStreaming ? Icons.play_arrow : Icons.stop,
            color: Colors.white,
          ),
          style: ElevatedButton.styleFrom(
            backgroundColor: !isStreaming ? Colors.blue : Colors.red,
          ),
          label: Text(
            !isStreaming ? "Start Streaming" : "Stop Streaming",
            style: TextStyle(color: Colors.white),
          ),
          onPressed: isControllerInitialized
              ? () {
                  if (!isStreaming) {
                    startVideoStreaming();
                  } else {
                    stopVideoStreaming();
                  }
                }
              : null,
        ),
        if (Platform.isIOS || Platform.isAndroid) ...[
          ElevatedButton.icon(
            icon: Icon(
              !_isMultiStreaming ? Icons.hub : Icons.stop_circle,
              color: Colors.white,
            ),
            style: ElevatedButton.styleFrom(
              backgroundColor: !_isMultiStreaming ? Colors.teal : Colors.red,
            ),
            label: Text(
              !_isMultiStreaming
                  ? 'Start Multi Streaming'
                  : 'Stop Multi Streaming',
              style: const TextStyle(color: Colors.white),
            ),
            onPressed: isControllerInitialized
                ? () {
                    if (!_isMultiStreaming) {
                      startMultiVideoStreaming();
                    } else {
                      stopMultiVideoStreaming();
                    }
                  }
                : null,
          ),
          if (_isMultiStreaming)
            ElevatedButton.icon(
              icon: const Icon(Icons.link_off, color: Colors.white),
              style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
              label: const Text(
                'Stop dest1 only',
                style: TextStyle(color: Colors.white),
              ),
              onPressed: isControllerInitialized ? stopMultiDestination1 : null,
            ),
        ],
        // Only Android has implemented it
        if (Platform.isAndroid)
          ElevatedButton.icon(
            icon: Icon(
              isRecordingPaused ? Icons.play_arrow : Icons.pause,
              color: Colors.white,
            ),
            style: ElevatedButton.styleFrom(
              backgroundColor: !isRecordingPaused ? Colors.blue : Colors.red,
            ),
            label: Text(
              !isRecordingPaused ? "pause Recording" : "resume Recording",
              style: TextStyle(color: Colors.white),
            ),
            onPressed: isControllerInitialized && isRecordingVideo
                ? () async {
                    if (isRecordingPaused) {
                      await resumeVideoRecording();
                    } else {
                      await pauseVideoRecording();
                    }
                  }
                : null,
          ),
        // stop all Streaming and Recording
        ElevatedButton.icon(
          icon: Icon(
            !(isRecordingVideo && isStreaming) ? Icons.play_arrow : Icons.stop,
            color: Colors.white,
          ),
          style: ElevatedButton.styleFrom(
            backgroundColor: !(isRecordingVideo && isStreaming)
                ? Colors.blue
                : Colors.red,
          ),
          label: Text(
            !(isRecordingVideo && isStreaming)
                ? "Start Streaming And Recording"
                : "Stop Streaming And Recording",
            style: TextStyle(color: Colors.white),
          ),
          onPressed: isControllerInitialized
              ? () {
                  if (isRecordingVideo && isStreaming) {
                    stopRecordingOrStreaming();
                  } else {
                    onRecordingAndVideoStreamingButtonPressed();
                  }
                }
              : null,
        ),
        _cameraTogglesRowWidget(),
        const SizedBox(width: 5),
        Text('${enableAudio ? 'Enable' : 'Disable'} Audio'),
        Switch(
          value: enableAudio,
          onChanged: (bool value) async {
            if (isControllerInitialized) {
              await controller.setAudioMuted(!value);
              setState(() {
                enableAudio = value;
              });
            } else {
              showInSnackBar('Please select a camera first.');
            }
          },
        ),
        const SizedBox(width: 5),
        Text('${isFlashLight ? 'Enable' : 'Disable'} FlashLight'),
        Switch(
          value: isFlashLight,
          onChanged: (bool value) async {
            if (isControllerInitialized &&
                _cameraDesc?.lensDirection == CameraLensDirection.back) {
              setState(() {
                isFlashLight = value;
              });
              await controller.switchFlashLight(value);
            } else {
              showInSnackBar('Please select a camera first.');
            }
          },
        ),
        if (Platform.isAndroid) ...[
          const SizedBox(height: 8),
          DropdownButton<VideoSourceType>(
            value: _videoSource,
            items: VideoSourceType.values
                .map(
                  (source) => DropdownMenuItem(
                    value: source,
                    child: Text('Source: ${source.name}'),
                  ),
                )
                .toList(),
            onChanged:
                isControllerInitialized && !isStreaming && !isRecordingVideo
                ? (source) async {
                    if (source == null) return;
                    try {
                      if (source == VideoSourceType.screen) {
                        await controller.requestScreenCapture();
                      }
                      await controller.setVideoSource(source);
                      if (mounted) setState(() => _videoSource = source);
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
          ),
          DropdownButton<VideoCodecType>(
            value: _videoCodec,
            items: VideoCodecType.values
                .map(
                  (codec) => DropdownMenuItem(
                    value: codec,
                    child: Text('Video: ${codec.name}'),
                  ),
                )
                .toList(),
            onChanged:
                isControllerInitialized && !isStreaming && !isRecordingVideo
                ? (codec) async {
                    if (codec == null) return;
                    try {
                      await controller.setVideoCodec(codec);
                      if (mounted) setState(() => _videoCodec = codec);
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
          ),
          DropdownButton<AudioCodecType>(
            value: _audioCodec,
            hint: const Text('Audio: AAC / WHIP OPUS'),
            items: AudioCodecType.values
                .map(
                  (codec) => DropdownMenuItem(
                    value: codec,
                    child: Text('Audio: ${codec.name}'),
                  ),
                )
                .toList(),
            onChanged:
                isControllerInitialized && !isStreaming && !isRecordingVideo
                ? (codec) async {
                    if (codec == null) return;
                    try {
                      await controller.setAudioCodec(codec);
                      if (mounted) setState(() => _audioCodec = codec);
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
          ),
          ElevatedButton(
            onPressed: isControllerInitialized && !_isMultiStreaming
                ? () async {
                    try {
                      await controller.tapToFocus();
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
            child: const Text('Focus center'),
          ),
          ElevatedButton(
            onPressed: isControllerInitialized && !_isMultiStreaming
                ? () async {
                    try {
                      final controls = await controller.getCameraControls();
                      await controller.setZoom(
                        2.0.clamp(controls.minZoom, controls.maxZoom),
                      );
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
            child: const Text('Zoom 2×'),
          ),
          ElevatedButton(
            onPressed:
                isControllerInitialized && (isStreaming || isRecordingVideo)
                ? () async {
                    try {
                      await controller.requestKeyFrame();
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
            child: const Text('Request keyframe'),
          ),
          Text('BT.709 色彩 (RootEncoder 2.7+)'),
          Switch(
            value: _forceBt709,
            onChanged: isStreaming || isRecordingVideo
                ? null
                : (v) {
                    setState(() => _forceBt709 = v);
                  },
          ),
          Text('RTMP Ping / RTT (需推流前开启)'),
          Switch(
            value: _rtmpShouldSendPings,
            onChanged: (v) {
              setState(() => _rtmpShouldSendPings = v);
            },
          ),
          Text('变调 PitchShift (${_pitchShift.toStringAsFixed(1)})'),
          Slider(
            value: _pitchShift,
            min: 0.5,
            max: 3.0,
            divisions: 25,
            label: _pitchShift.toStringAsFixed(1),
            onChanged: isControllerInitialized
                ? (v) async {
                    setState(() => _pitchShift = v);
                    try {
                      await controller.setPitchShift(v);
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
          ),
          Text('曝光锁定 (预览/推流后)'),
          Switch(
            value: _exposureLocked,
            onChanged: isControllerInitialized
                ? (v) async {
                    try {
                      if (v) {
                        final ok = await controller.lockExposure();
                        setState(() => _exposureLocked = ok);
                        if (!ok) {
                          showInSnackBar(
                            'Exposure lock failed (unsupported or camera not started)',
                          );
                        }
                      } else {
                        await controller.unlockExposure();
                        setState(() => _exposureLocked = false);
                      }
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
          ),
          if (_androidStreamStatsLine.isNotEmpty)
            Padding(
              padding: const EdgeInsets.only(top: 8.0),
              child: Text(
                _androidStreamStatsLine,
                style: const TextStyle(fontSize: 12),
              ),
            ),
        ],
        ElevatedButton(
          onPressed: !isControllerInitialized
              ? null
              : () async {
                  try {
                    await controller.setOverlayText(
                      text: 'LIVE',
                      fontSize: 36,
                      colorArgb: 0xFFFF0000,
                      position: OverlayPosition.topLeft,
                    );
                  } on CameraException catch (e) {
                    _showCameraException(e);
                  }
                },
          child: const Text('叠字 LIVE'),
        ),
        ElevatedButton(
          onPressed: !isControllerInitialized
              ? null
              : () async {
                  try {
                    await controller.clearOverlay();
                  } on CameraException catch (e) {
                    _showCameraException(e);
                  }
                },
          child: const Text('清除叠字'),
        ),
        if (Platform.isIOS) ...[
          const SizedBox(height: 8),
          const Text('多任务相机 (HaishinKit 2.2.5+, iOS 17+)'),
          Switch(
            value: _iosMultitaskingCamera,
            onChanged:
                _capabilities?.supports('multitaskingCameraAccess') != true
                ? null
                : (bool v) async {
                    setState(() => _iosMultitaskingCamera = v);
                    if (!isControllerInitialized) {
                      return;
                    }
                    try {
                      await controller.setMultitaskingCameraAccessEnabled(v);
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  },
          ),
          ElevatedButton(
            onPressed: isControllerInitialized
                ? () async {
                    try {
                      await controller.setVideoSettings(
                        expectedFrameRate: 30,
                        bitRateMode: 'average',
                      );
                      showInSnackBar(
                        '已设置 expectedFrameRate=30、bitRateMode=average(2.2.2 onMetaData / 2.2.1 VBR 基础)',
                      );
                    } on CameraException catch (e) {
                      _showCameraException(e);
                    }
                  }
                : null,
            child: const Text('HaishinKit 视频编码示例'),
          ),
        ],

        _thumbnailWidget(),
      ],
    );
  }

  // switch cameras
  void onSwitchCameras(CameraDescription? cld) async {
    if (cld == null) {
      showInSnackBar("camersa not Empty");
      return;
    }
    try {
      setState(() {
        _cameraDesc = cld;
      });
      await controller.switchCamera(cld.name!);
      await WakelockPlus.enable();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  /// Display a row of toggles to select the camera (or a message if no camera is available).
  Widget _cameraTogglesRowWidget() {
    if (cameras.isEmpty) {
      return Text('No camera found');
    } else {
      return RadioGroup<CameraDescription>(
        groupValue: _cameraDesc,
        onChanged: (CameraDescription? cld) {
          if (isControllerInitialized) {
            onSwitchCameras(cld);
          }
        },
        child: Column(
          children: cameras.map((cameraDescription) {
            return SizedBox(
              width: 90.0,
              child: RadioListTile<CameraDescription>(
                title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
                value: cameraDescription,
              ),
            );
          }).toList(),
        ),
      );
    }
  }

  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

  void showInSnackBar(String message) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  //init
  void onInit() {
    if (cameras.isEmpty) {
      showInSnackBar("No available cameras");
      return;
    }
    final cameraItem = cameras.firstWhere(
      (camera) => camera.lensDirection == CameraLensDirection.front,
      orElse: () => cameras.first,
    );
    setState(() {
      _cameraDesc = cameraItem;
    });
    // init cameras index 0
    onNewCameraSelected(cameraItem);
  }

  //init camera
  void onNewCameraSelected(CameraDescription? cameraDescription) async {
    if (cameraDescription == null) return;
    if (Platform.isMacOS == false) {
      await Permission.camera.request();
      await Permission.microphone.request();
      if (Platform.isAndroid) await Permission.accessLocalNetwork.request();
    }
    try {
      if (isControllerInitialized) {
        await controller.switchCamera(cameraDescription.name!);
      } else {
        await controller.initialize(cameraDescription);
      }
      _capabilities = await controller.getCapabilities();
      if (Platform.isIOS && _iosMultitaskingCamera) {
        try {
          await controller.setMultitaskingCameraAccessEnabled(true);
        } on CameraException catch (e) {
          _showCameraException(e);
        }
      }
    } on CameraException catch (e) {
      _showCameraException(e);
    }
    // If the controller is updated then update the UI.
    controller.addListener(() {
      if (mounted) setState(() {});

      if (controller.value.event != null) {
        final Map<dynamic, dynamic> event =
            controller.value.event as Map<dynamic, dynamic>;
        final String eventType = event['eventType'] as String;
        //只有发生错误的时候才
        if ((eventType == "error" || eventType == 'rtmp_stopped') &&
            isStreaming) {
          showInSnackBar('Camera error ${controller.value.errorDescription}');
          stopVideoStreaming();
        } else {
          debugPrint('Event $event');
          showInSnackBar('Camera message ${controller.value.errorDescription}');
        }
      }
    });

    if (mounted) {
      setState(() {});
    }
  }

  void onTakePictureButtonPressed() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error:  not init');
      return;
    }
    final Directory? extDir = Platform.isAndroid
        ? await getExternalStorageDirectory()
        : await getTemporaryDirectory();
    if (extDir == null) {
      return;
    }
    final String dirPath = '${extDir.path}/Pictures/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.jpg';

    if (isTakingPicture) {
      showInSnackBar('Error:  current is TakingPicture');
      // A capture is already pending, do nothing.
      return;
    }

    try {
      await controller.takePicture(filePath);
      if (mounted) {
        setState(() {
          imagePath = filePath;
        });
        showInSnackBar('Picture saved to $filePath');
      }
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  void onVideoRecordButtonPressed() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error: not init');
      return;
    }

    final Directory? extDir = Platform.isAndroid
        ? await getExternalStorageDirectory()
        : await getTemporaryDirectory();
    if (extDir == null) return;

    final String dirPath = '${extDir.path}/Movies/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.mp4';

    if (isRecordingVideo) {
      // A recording is already started, do nothing.
      showInSnackBar('Error: Recording Video');
      return;
    }

    try {
      if (Platform.isAndroid) {
        await controller.setForceBt709Color(_forceBt709);
      }
      videoPath = filePath;
      await controller.startVideoRecording(filePath);
      showInSnackBar('Saving video to $filePath');
      await WakelockPlus.enable();
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
  }

  void startVideoStreaming() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error: select a camera first.');
      return;
    }

    if (isStreaming) {
      showInSnackBar('Error: is Streaming');
      return;
    }

    // Open up a dialog for the url
    String myUrl = await _getUrl();
    if (myUrl.isEmpty) {
      showInSnackBar('url is empty');
      return;
    }
    try {
      if (Platform.isAndroid) {
        // await controller.setVideoSource(VideoSourceType.cameraX);
        // await controller.setVideoCodec(VideoCodecType.h265);
        // await controller.setAudioCodec(AudioCodecType.aac);

        // await controller.setAudioSettings(128 * 1024);
        // await controller.setVideoSettings(bitrate: 5 * 1024 * 1024);
        // await controller.setFrameRate(30);
        await controller.setForceBt709Color(_forceBt709);
        await controller.setRtmpShouldSendPings(_rtmpShouldSendPings);
        await controller.setRtmpMetadata({
          'application': 'rtmp_streaming_example',
        });
      }
      await controller.startVideoStreaming(myUrl, protocol: _selectedProtocol);
      // await controller?.unlockExposure();
      // await controller?.unlockWhiteBalance();
      setState(() => _isMultiStreaming = false);
      showInSnackBar('Streaming (${_selectedProtocol.name}) video to $myUrl');
      await WakelockPlus.enable();
      _startAndroidStreamStatsTimer();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  /// Demo: push the same capture to two destinations (excludes WHIP/WHEP).
  void startMultiVideoStreaming() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error: select a camera first.');
      return;
    }
    if (isStreaming) {
      showInSnackBar('Error: already streaming — stop first.');
      return;
    }

    final destinations = await _getMultiStreamDestinations();
    if (destinations == null || destinations.length < 2) {
      showInSnackBar('需要至少两个有效推流地址');
      return;
    }
    try {
      if (Platform.isAndroid) {
        await controller.setForceBt709Color(_forceBt709);
        await controller.setRtmpShouldSendPings(_rtmpShouldSendPings);
        await controller.setRtmpMetadata({
          'application': 'rtmp_streaming_example',
        });
      }
      await controller.startMultiStreaming(destinations);
      setState(() => _isMultiStreaming = true);
      final summary = destinations
          .map((d) => '${d.id ?? "?"}:${d.protocol.name}')
          .join(', ');
      showInSnackBar('Multi streaming → $summary');
      await WakelockPlus.enable();
      _startAndroidStreamStatsTimer();
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Future<void> stopMultiVideoStreaming() async {
    try {
      await controller.stopMultiStreaming();
      setState(() => _isMultiStreaming = false);
      _stopAndroidStreamStatsTimer();
      await WakelockPlus.disable();
      showInSnackBar('Multi streaming stopped');
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Future<void> stopMultiDestination1() async {
    try {
      await controller.stopStreamingDestination('dest1');
      showInSnackBar('Stopped destination dest1 (dest2 may still be live)');
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  void onRecordingAndVideoStreamingButtonPressed() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error: not init');
      return;
    }

    if (isStreaming) {
      showInSnackBar('Error: is Streaming');
      return;
    }

    String myUrl = await _getUrl();
    if (myUrl.isEmpty) {
      showInSnackBar('url is empty');
      return;
    }
    final Directory? extDir = Platform.isAndroid
        ? await getExternalStorageDirectory()
        : await getTemporaryDirectory();
    if (extDir == null) {
      return;
    }
    final String dirPath = '${extDir.path}/Movies/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.mp4';

    try {
      if (Platform.isAndroid) {
        await controller.setForceBt709Color(_forceBt709);
        await controller.setRtmpShouldSendPings(_rtmpShouldSendPings);
        await controller.setRtmpMetadata({
          'application': 'rtmp_streaming_example',
        });
      }
      videoPath = filePath;
      await controller.startVideoRecordingAndStreaming(
        videoPath!,
        myUrl,
        protocol: _selectedProtocol,
      );
      showInSnackBar(
        'Recording streaming (${_selectedProtocol.name}) video to $myUrl',
      );
      await WakelockPlus.enable();
      _startAndroidStreamStatsTimer();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  // stop video
  void _startAndroidStreamStatsTimer() {
    if (!Platform.isAndroid) return;
    _streamStatsTimer?.cancel();
    _streamStatsTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
      if (!mounted || !isStreaming) return;
      try {
        final s = await controller.getStreamStatistics();
        if (!mounted) return;
        setState(() {
          _androidStreamStatsLine =
              'fps=${s.fps}  RTT=${s.rttMicros}µs  已发送字节=${s.bytesSend}';
        });
      } catch (_) {}
    });
  }

  void _stopAndroidStreamStatsTimer() {
    _streamStatsTimer?.cancel();
    _streamStatsTimer = null;
    if (mounted) {
      setState(() => _androidStreamStatsLine = '');
    }
  }

  void stopRecordingOrStreaming() async {
    if (!isStreaming && !isRecordingVideo) {
      showInSnackBar('Video stop streamed or recording');
      return;
    }
    try {
      await controller.stopRecordingOrStreaming();
      setState(() => _isMultiStreaming = false);
      _stopAndroidStreamStatsTimer();
      await WakelockPlus.disable();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  // stop Video Recording
  void stopVideoRecording() async {
    if (!isRecordingVideo) {
      showInSnackBar('not Start Recording Video');
      return;
    }
    try {
      await controller.stopVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  /// pause Video Recording
  /// Only Android has implemented it
  Future<void> pauseVideoRecording() async {
    try {
      if (!isRecordingVideo) {
        showInSnackBar('not Start Video recording');
        return;
      }
      await controller.pauseVideoRecording();
      showInSnackBar('Video recording paused');
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    } catch (e) {
      showInSnackBar(e.toString());
    }
  }

  /// resume Video Recording
  /// Only Android has implemented it
  Future<void> resumeVideoRecording() async {
    try {
      if (!isRecordingVideo) {
        showInSnackBar('not Start Video recording');
        return;
      }
      await controller.resumeVideoRecording();
      showInSnackBar('Video recording resume');
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    } catch (e) {
      showInSnackBar(e.toString());
    }
  }

  Future<String> _getUrl() async {
    // Open up a dialog for the url + protocol
    String result = _textFieldController.text;
    StreamingProtocol selected = _selectedProtocol;
    if (!_availableProtocols.contains(selected)) {
      selected = StreamingProtocol.rtmp;
    }

    return await showDialog(
      context: context,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            return AlertDialog(
              title: Text('Url to Stream to'),
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  DropdownButtonFormField<StreamingProtocol>(
                    key: ValueKey(selected),
                    initialValue: selected,
                    decoration: const InputDecoration(labelText: 'Protocol'),
                    items: _availableProtocols
                        .map(
                          (p) => DropdownMenuItem(
                            value: p,
                            child: Text(p.name.toUpperCase()),
                          ),
                        )
                        .toList(),
                    onChanged: (value) {
                      if (value == null) return;
                      setDialogState(() {
                        selected = value;
                        // Seed a typical URL for the chosen protocol.
                        switch (value) {
                          case StreamingProtocol.rtmp:
                            _textFieldController.text =
                                'rtmp://192.168.1.15/live/live';
                          case StreamingProtocol.rtsp:
                            _textFieldController.text =
                                'rtsp://192.168.1.15:8554/live';
                          case StreamingProtocol.srt:
                            _textFieldController.text =
                                'srt://192.168.1.15:10080?streamid=#!::r=live/livestream,m=publish';
                          case StreamingProtocol.udp:
                            _textFieldController.text =
                                'udp://192.168.1.15:5004';
                          case StreamingProtocol.whip:
                            _textFieldController.text =
                                'https://example.com/whip';
                          case StreamingProtocol.whep:
                            _textFieldController.text =
                                'https://example.com/whep';
                        }
                        result = _textFieldController.text;
                      });
                    },
                  ),
                  TextField(
                    controller: _textFieldController,
                    decoration: const InputDecoration(
                      hintText: "Url to Stream to",
                    ),
                    onChanged: (String str) => result = str,
                  ),
                ],
              ),
              actions: <Widget>[
                TextButton(
                  child: Text(
                    MaterialLocalizations.of(context).cancelButtonLabel,
                  ),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                ),
                TextButton(
                  child: Text(MaterialLocalizations.of(context).okButtonLabel),
                  onPressed: () {
                    _selectedProtocol = selected;
                    Navigator.pop(context, result);
                  },
                ),
              ],
            );
          },
        );
      },
    );
  }

  void stopVideoStreaming() async {
    if (!isControllerInitialized) {
      showInSnackBar('Error: not init');
      return;
    }
    if (!isStreaming) {
      showInSnackBar('Error: not is Streaming');
      return;
    }

    try {
      await controller.stopVideoStreaming();
      setState(() => _isMultiStreaming = false);
      _stopAndroidStreamStatsTimer();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  Future<List<StreamDestination>?> _getMultiStreamDestinations() async {
    var protocol1 = _multiProtocol1;
    var protocol2 = _multiProtocol2;
    var url1 = _multiUrl1Controller.text;
    var url2 = _multiUrl2Controller.text;

    String sampleUrl(StreamingProtocol p, {required bool second}) {
      switch (p) {
        case StreamingProtocol.rtsp:
          return second
              ? 'rtsp://192.168.2.81:8554/live2'
              : 'rtsp://192.168.2.81:8554/live';
        case StreamingProtocol.srt:
          return second
              ? 'srt://192.168.2.81:10080?streamid=#!::r=live/livestream2,m=publish'
              : 'srt://192.168.2.81:10080?streamid=#!::r=live/livestream,m=publish';
        case StreamingProtocol.rtmp:
          return second
              ? 'rtmp://192.168.2.81/live/live2'
              : 'rtmp://192.168.2.81/live/live';
        case StreamingProtocol.udp:
          return second ? 'udp://192.168.2.81:5005' : 'udp://192.168.2.81:5004';
        case StreamingProtocol.whip:
        case StreamingProtocol.whep:
          return '';
      }
    }

    return showDialog<List<StreamDestination>>(
      context: context,
      builder: (context) {
        return StatefulBuilder(
          builder: (context, setDialogState) {
            Widget protocolField({
              required String label,
              required StreamingProtocol value,
              required ValueChanged<StreamingProtocol> onChanged,
            }) {
              return DropdownButtonFormField<StreamingProtocol>(
                key: ValueKey('$label-$value'),
                initialValue: value,
                decoration: InputDecoration(labelText: label),
                items: _multiProtocols
                    .map(
                      (p) => DropdownMenuItem(
                        value: p,
                        child: Text(p.name.toUpperCase()),
                      ),
                    )
                    .toList(),
                onChanged: _multiProtocols.length <= 1
                    ? null
                    : (v) {
                        if (v != null) onChanged(v);
                      },
              );
            }

            const hint = 'iOS 多路示例:一路 RTMP + 一路 SRT';

            return AlertDialog(
              title: const Text('多路推流(双目标)'),
              content: SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(hint, style: const TextStyle(fontSize: 12)),
                    const SizedBox(height: 12),
                    protocolField(
                      label: 'dest1 协议',
                      value: protocol1,
                      onChanged: (v) => setDialogState(() {
                        protocol1 = v;
                        _multiUrl1Controller.text = sampleUrl(v, second: false);
                        url1 = _multiUrl1Controller.text;
                      }),
                    ),
                    TextField(
                      controller: _multiUrl1Controller,
                      decoration: InputDecoration(
                        labelText: 'dest1 URL',
                        hintText: sampleUrl(protocol1, second: false),
                      ),
                      onChanged: (s) => url1 = s,
                    ),
                    const SizedBox(height: 12),
                    protocolField(
                      label: 'dest2 协议',
                      value: protocol2,
                      onChanged: (v) => setDialogState(() {
                        protocol2 = v;
                        _multiUrl2Controller.text = sampleUrl(v, second: true);
                        url2 = _multiUrl2Controller.text;
                      }),
                    ),
                    TextField(
                      controller: _multiUrl2Controller,
                      decoration: InputDecoration(
                        labelText: 'dest2 URL',
                        hintText: sampleUrl(protocol2, second: true),
                      ),
                      onChanged: (s) => url2 = s,
                    ),
                  ],
                ),
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: Text(
                    MaterialLocalizations.of(context).cancelButtonLabel,
                  ),
                ),
                TextButton(
                  onPressed: () {
                    _multiProtocol1 = protocol1;
                    _multiProtocol2 = protocol2;
                    Navigator.pop(context, <StreamDestination>[
                      StreamDestination(
                        url: url1.trim(),
                        protocol: protocol1,
                        id: 'dest1',
                      ),
                      StreamDestination(
                        url: url2.trim(),
                        protocol: protocol2,
                        id: 'dest2',
                      ),
                    ]);
                  },
                  child: Text(MaterialLocalizations.of(context).okButtonLabel),
                ),
              ],
            );
          },
        );
      },
    );
  }

  //
  void _showCameraException(CameraException e) {
    logError(e.code, e.description ?? "No description found");
    showInSnackBar(
      'Error: ${e.code}\n${e.description ?? "No description found"}',
    );
  }
}

class CameraApp extends StatelessWidget {
  const CameraApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CameraExampleHome());
  }
}

List<CameraDescription> cameras = [];

Future<void> main() async {
  // Fetch the available cameras before initializing the app.
  try {
    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();
    debugPrint("$cameras");
  } on CameraException catch (e) {
    logError(e.code, e.description ?? "No description found");
  }
  runApp(CameraApp());
}
9
likes
160
points
653
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for Camera and Microphone streaming library via RTMP.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on rtmp_streaming

Packages that implement rtmp_streaming