删除未使用的相机和记录类来重构记录功能;更新pubspec中的依赖项。Yaml为本地插件路径;简化MainActivity和相关类,以提高性能和可维护性。
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export 'src/apivideo_camera_preview.dart';
|
||||
export 'src/apivideo_live_stream_controller.dart';
|
||||
export 'src/apivideo_live_stream_mobile_platform.dart';
|
||||
export 'src/types.dart';
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:native_device_orientation/native_device_orientation.dart';
|
||||
|
||||
import 'apivideo_live_stream_controller.dart';
|
||||
|
||||
/// Widget that displays the camera preview of [controller].
|
||||
///
|
||||
class ApiVideoCameraPreview extends StatefulWidget {
|
||||
/// Creates a new [ApiVideoCameraPreview] instance for [controller] and a [child] overlay.
|
||||
const ApiVideoCameraPreview(
|
||||
{super.key,
|
||||
required this.controller,
|
||||
this.fit = BoxFit.contain,
|
||||
this.child});
|
||||
|
||||
/// The controller for the camera to display the preview for.
|
||||
final ApiVideoLiveStreamController controller;
|
||||
|
||||
/// The [BoxFit] for the video. The [child] is scale to the preview box.
|
||||
final BoxFit fit;
|
||||
|
||||
/// A widget to overlay on top of the camera preview. It is scaled to the camera preview [FittedBox].
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
State<ApiVideoCameraPreview> createState() => _ApiVideoCameraPreviewState();
|
||||
}
|
||||
|
||||
class _ApiVideoCameraPreviewState extends State<ApiVideoCameraPreview> {
|
||||
_ApiVideoCameraPreviewState() {
|
||||
_widgetListener = ApiVideoLiveStreamWidgetListener(onTextureReady: () {
|
||||
final int newTextureId = widget.controller.textureId;
|
||||
if (newTextureId != _textureId) {
|
||||
setState(() {
|
||||
_textureId = newTextureId;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_eventsListener =
|
||||
ApiVideoLiveStreamEventsListener(onVideoSizeChanged: (size) {
|
||||
_updateAspectRatio(size);
|
||||
});
|
||||
}
|
||||
|
||||
late ApiVideoLiveStreamWidgetListener _widgetListener;
|
||||
late ApiVideoLiveStreamEventsListener _eventsListener;
|
||||
late int _textureId;
|
||||
|
||||
double _aspectRatio = 1.77;
|
||||
Size _size = const Size(1280, 720);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textureId = widget.controller.textureId;
|
||||
widget.controller.addWidgetListener(_widgetListener);
|
||||
widget.controller.addEventsListener(_eventsListener);
|
||||
if (widget.controller.isInitialized) {
|
||||
widget.controller.videoSize.then((size) {
|
||||
if (size != null) {
|
||||
_updateAspectRatio(size);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.stopPreview();
|
||||
widget.controller.removeWidgetListener(_widgetListener);
|
||||
widget.controller.removeEventsListener(_eventsListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _textureId == ApiVideoLiveStreamController.kUninitializedTextureId
|
||||
? Container()
|
||||
: _buildPreview(context);
|
||||
}
|
||||
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
return NativeDeviceOrientationReader(builder: (context) {
|
||||
final orientation = NativeDeviceOrientationReader.orientation(context);
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(alignment: Alignment.center, children: [
|
||||
_buildFittedPreview(constraints, orientation),
|
||||
_buildFittedOverlay(constraints, orientation)
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFittedPreview(
|
||||
BoxConstraints constraints, NativeDeviceOrientation orientation) {
|
||||
final orientedSize = _size.orientate(orientation);
|
||||
// See https://github.com/flutter/flutter/issues/17287
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxHeight,
|
||||
child: FittedBox(
|
||||
fit: widget.fit,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: orientedSize.width,
|
||||
height: orientedSize.height,
|
||||
child: _wrapInRotatedBox(
|
||||
orientation: orientation,
|
||||
child: widget.controller.buildPreview())))));
|
||||
}
|
||||
|
||||
Widget _buildFittedOverlay(
|
||||
BoxConstraints constraints, NativeDeviceOrientation orientation) {
|
||||
final orientedSize = _size.orientate(orientation);
|
||||
final fittedSize =
|
||||
applyBoxFit(widget.fit, orientedSize, constraints.biggest);
|
||||
return SizedBox(
|
||||
width: fittedSize.destination.width,
|
||||
height: fittedSize.destination.height,
|
||||
child: widget.child ?? Container());
|
||||
}
|
||||
|
||||
Widget _wrapInRotatedBox(
|
||||
{required NativeDeviceOrientation orientation, required Widget child}) {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return RotatedBox(
|
||||
quarterTurns: orientation.getQuarterTurns(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateAspectRatio(Size newSize) async {
|
||||
final double newAspectRatio = newSize.aspectRatio;
|
||||
if ((newAspectRatio != _aspectRatio) || (newSize != _size)) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_size = newSize;
|
||||
_aspectRatio = newAspectRatio;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OrientationHelper on NativeDeviceOrientation {
|
||||
/// Returns true if the orientation is portrait.
|
||||
bool isLandscape() {
|
||||
return [
|
||||
NativeDeviceOrientation.landscapeLeft,
|
||||
NativeDeviceOrientation.landscapeRight
|
||||
].contains(this);
|
||||
}
|
||||
|
||||
/// Returns the number of clockwise quarter turns the orientation is rotated
|
||||
int getQuarterTurns() {
|
||||
Map<NativeDeviceOrientation, int> turns = {
|
||||
NativeDeviceOrientation.unknown: 0,
|
||||
NativeDeviceOrientation.portraitUp: 0,
|
||||
NativeDeviceOrientation.landscapeRight: 1,
|
||||
NativeDeviceOrientation.portraitDown: 2,
|
||||
NativeDeviceOrientation.landscapeLeft: 3,
|
||||
};
|
||||
return turns[this]!;
|
||||
}
|
||||
}
|
||||
|
||||
extension OrientedSize on Size {
|
||||
/// Returns the size with width and height swapped if [orientation] is portrait.
|
||||
Size orientate(NativeDeviceOrientation orientation) {
|
||||
if (orientation.isLandscape()) {
|
||||
return Size(width, height);
|
||||
} else {
|
||||
return Size(height, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:apivideo_live_stream/apivideo_live_stream.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'apivideo_live_stream_platform_interface.dart';
|
||||
import 'types.dart';
|
||||
|
||||
ApiVideoLiveStreamPlatform get _platform {
|
||||
return ApiVideoLiveStreamPlatform.instance;
|
||||
}
|
||||
|
||||
/// Controller of the live streaming
|
||||
class ApiVideoLiveStreamController {
|
||||
final VideoConfig _initialVideoConfig;
|
||||
final AudioConfig _initialAudioConfig;
|
||||
final CameraPosition _initialCameraPosition;
|
||||
|
||||
static const int kUninitializedTextureId = -1;
|
||||
int _textureId = kUninitializedTextureId;
|
||||
|
||||
/// This is just exposed for testing. Do not use it.
|
||||
@internal
|
||||
int get textureId => _textureId;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// Gets the current state of the video player.
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
/// Events
|
||||
StreamSubscription<dynamic>? _eventSubscription;
|
||||
List<ApiVideoLiveStreamEventsListener> _eventsListeners = [];
|
||||
List<ApiVideoLiveStreamWidgetListener> _widgetListeners = [];
|
||||
|
||||
/// Creates a new [ApiVideoLiveStreamController] instance.
|
||||
ApiVideoLiveStreamController(
|
||||
{required AudioConfig initialAudioConfig,
|
||||
required VideoConfig initialVideoConfig,
|
||||
CameraPosition initialCameraPosition = CameraPosition.back,
|
||||
VoidCallback? onConnectionSuccess,
|
||||
Function(String)? onConnectionFailed,
|
||||
VoidCallback? onDisconnection,
|
||||
Function(Exception)? onError})
|
||||
: _initialVideoConfig = initialVideoConfig,
|
||||
_initialAudioConfig = initialAudioConfig,
|
||||
_initialCameraPosition = initialCameraPosition {
|
||||
_eventsListeners.add(ApiVideoLiveStreamEventsListener(
|
||||
onConnectionSuccess: onConnectionSuccess,
|
||||
onConnectionFailed: onConnectionFailed,
|
||||
onDisconnection: onDisconnection,
|
||||
onError: onError));
|
||||
}
|
||||
|
||||
ApiVideoLiveStreamController.fromListener(
|
||||
{required AudioConfig initialAudioConfig,
|
||||
required VideoConfig initialVideoConfig,
|
||||
CameraPosition initialCameraPosition = CameraPosition.back,
|
||||
ApiVideoLiveStreamEventsListener? listener})
|
||||
: _initialVideoConfig = initialVideoConfig,
|
||||
_initialAudioConfig = initialAudioConfig,
|
||||
_initialCameraPosition = initialCameraPosition {
|
||||
if (listener != null) {
|
||||
_eventsListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new live stream instance with initial audio and video configurations.
|
||||
Future<void> initialize() async {
|
||||
_textureId = await _platform.initialize() ?? kUninitializedTextureId;
|
||||
|
||||
_eventSubscription = _platform
|
||||
.liveStreamingEventsFor(_textureId)
|
||||
.listen(_eventListener, onError: _errorListener);
|
||||
|
||||
for (var listener in [..._widgetListeners]) {
|
||||
if (listener.onTextureReady != null) {
|
||||
listener.onTextureReady!();
|
||||
}
|
||||
}
|
||||
|
||||
await setCameraPosition(_initialCameraPosition);
|
||||
await setVideoConfig(_initialVideoConfig);
|
||||
await setAudioConfig(_initialAudioConfig);
|
||||
|
||||
await startPreview();
|
||||
_isInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/// Disposes the live stream instance.
|
||||
Future<void> dispose() async {
|
||||
await _eventSubscription?.cancel();
|
||||
_eventsListeners.clear();
|
||||
_widgetListeners.clear();
|
||||
await _platform.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
/// Sets new video parameters.
|
||||
///
|
||||
/// Do not call when in live or when preview is running.
|
||||
Future<void> setVideoConfig(VideoConfig videoConfig) {
|
||||
return _platform.setVideoConfig(videoConfig);
|
||||
}
|
||||
|
||||
/// Sets new audio parameters.
|
||||
///
|
||||
/// Do not call when in live or when preview is running.
|
||||
Future<void> setAudioConfig(AudioConfig audioConfig) {
|
||||
return _platform.setAudioConfig(audioConfig);
|
||||
}
|
||||
|
||||
/// Starts the live stream to the specified "[url]/[streamKey]".
|
||||
Future<void> startStreaming(
|
||||
{required String streamKey,
|
||||
String url = "rtmp://broadcast.api.video/s/"}) async {
|
||||
return _platform.startStreaming(streamKey: streamKey, url: url);
|
||||
}
|
||||
|
||||
/// Stops the live stream.
|
||||
Future<void> stopStreaming() {
|
||||
return _platform.stopStreaming();
|
||||
}
|
||||
|
||||
/// Starts the camera preview.
|
||||
///
|
||||
/// The purpose of this method is to be called when application is sent
|
||||
/// to foreground.
|
||||
Future<void> startPreview() {
|
||||
return _platform.startPreview();
|
||||
}
|
||||
|
||||
/// Stops the camera preview.
|
||||
///
|
||||
/// The purpose of this method is to be called when application is sent
|
||||
/// to background.
|
||||
Future<void> stopPreview() {
|
||||
return _platform.stopPreview();
|
||||
}
|
||||
|
||||
/// Same as [stopStreaming] and [stopPreview]
|
||||
Future<void> stop() async {
|
||||
await stopStreaming();
|
||||
await stopPreview();
|
||||
}
|
||||
|
||||
/// Gets if live stream is streaming or not.
|
||||
Future<bool> get isStreaming {
|
||||
return _platform.getIsStreaming();
|
||||
}
|
||||
|
||||
/// Changes current back/front camera to front/back camera
|
||||
Future<void> switchCamera() async {
|
||||
final cameraPosition = await this.cameraPosition;
|
||||
if (cameraPosition == CameraPosition.back) {
|
||||
return setCameraPosition(CameraPosition.front);
|
||||
} else {
|
||||
return setCameraPosition(CameraPosition.back);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current camera position
|
||||
Future<CameraPosition> get cameraPosition {
|
||||
return _platform.getCameraPosition();
|
||||
}
|
||||
|
||||
/// Sets the current camera position
|
||||
Future<void> setCameraPosition(CameraPosition position) {
|
||||
return _platform.setCameraPosition(position);
|
||||
}
|
||||
|
||||
/// Lists back cameras exposed by Android CameraManager.
|
||||
Future<List<ApiVideoBackCamera>> getBackCameras() async {
|
||||
final cameras = await _platform.getBackCameras();
|
||||
return cameras
|
||||
.map(ApiVideoBackCamera.fromJson)
|
||||
.where((camera) => camera.cameraId.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Selects a concrete camera id. Android-only extension.
|
||||
Future<void> setCameraId(String cameraId) {
|
||||
return _platform.setCameraId(cameraId);
|
||||
}
|
||||
|
||||
/// Toggle mutes/unmutes from the microphone. See [isMuted] and [setIsMuted].
|
||||
Future<void> toggleMute() async {
|
||||
final isMuted = await this.isMuted;
|
||||
await setIsMuted(!isMuted);
|
||||
}
|
||||
|
||||
/// Gets if live stream is muted or not.
|
||||
Future<bool> get isMuted {
|
||||
return _platform.getIsMuted();
|
||||
}
|
||||
|
||||
/// Mutes/unmutes the microphone.
|
||||
Future<void> setIsMuted(bool isMuted) {
|
||||
return _platform.setIsMuted(isMuted);
|
||||
}
|
||||
|
||||
Future<Size?> get videoSize {
|
||||
return _platform.getVideoSize();
|
||||
}
|
||||
|
||||
/// Builds the preview widget.
|
||||
@internal
|
||||
Widget buildPreview() {
|
||||
return Texture(textureId: textureId);
|
||||
}
|
||||
|
||||
void addEventsListener(ApiVideoLiveStreamEventsListener listener) {
|
||||
_eventsListeners.add(listener);
|
||||
}
|
||||
|
||||
void removeEventsListener(ApiVideoLiveStreamEventsListener listener) {
|
||||
_eventsListeners.remove(listener);
|
||||
}
|
||||
|
||||
/// This is exposed for internal use only. Do not use it.
|
||||
@internal
|
||||
void addWidgetListener(ApiVideoLiveStreamWidgetListener listener) {
|
||||
_widgetListeners.add(listener);
|
||||
}
|
||||
|
||||
/// This is exposed for internal use only. Do not use it.
|
||||
@internal
|
||||
void removeWidgetListener(ApiVideoLiveStreamWidgetListener listener) {
|
||||
_widgetListeners.remove(listener);
|
||||
}
|
||||
|
||||
void _errorListener(Object obj) {
|
||||
final PlatformException e = obj as PlatformException;
|
||||
for (var listener in [..._eventsListeners]) {
|
||||
if (listener.onError != null) {
|
||||
listener.onError!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _eventListener(LiveStreamingEvent event) {
|
||||
switch (event.type) {
|
||||
case LiveStreamingEventType.connected:
|
||||
for (var listener in [..._eventsListeners]) {
|
||||
if (listener.onConnectionSuccess != null) {
|
||||
listener.onConnectionSuccess!();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LiveStreamingEventType.disconnected:
|
||||
for (var listener in [..._eventsListeners]) {
|
||||
if (listener.onDisconnection != null) {
|
||||
listener.onDisconnection!();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LiveStreamingEventType.connectionFailed:
|
||||
for (var listener in [..._eventsListeners]) {
|
||||
if (listener.onConnectionFailed != null) {
|
||||
listener.onConnectionFailed!(event.data as String);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LiveStreamingEventType.videoSizeChanged:
|
||||
for (var listener in [..._eventsListeners]) {
|
||||
if (listener.onVideoSizeChanged != null) {
|
||||
listener.onVideoSizeChanged!(event.data as Size);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case LiveStreamingEventType.unknown:
|
||||
// Nothing to do
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ApiVideoBackCamera {
|
||||
const ApiVideoBackCamera({
|
||||
required this.cameraId,
|
||||
required this.minFocalLength,
|
||||
required this.sensorWidth,
|
||||
required this.sensorHeight,
|
||||
required this.horizontalFov,
|
||||
});
|
||||
|
||||
final String cameraId;
|
||||
final double minFocalLength;
|
||||
final double sensorWidth;
|
||||
final double sensorHeight;
|
||||
final double horizontalFov;
|
||||
|
||||
factory ApiVideoBackCamera.fromJson(Map<String, dynamic> json) {
|
||||
return ApiVideoBackCamera(
|
||||
cameraId: json['cameraId'] as String? ?? '',
|
||||
minFocalLength: (json['minFocalLength'] as num?)?.toDouble() ?? 0,
|
||||
sensorWidth: (json['sensorWidth'] as num?)?.toDouble() ?? 0,
|
||||
sensorHeight: (json['sensorHeight'] as num?)?.toDouble() ?? 0,
|
||||
horizontalFov: (json['horizontalFov'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiVideoLiveStreamEventsListener {
|
||||
/// Gets notified when the connection is successful
|
||||
final VoidCallback? onConnectionSuccess;
|
||||
|
||||
/// Gets notified when the connection failed
|
||||
final Function(String)? onConnectionFailed;
|
||||
|
||||
/// Gets notified when the device has been disconnected
|
||||
final VoidCallback? onDisconnection;
|
||||
|
||||
/// Gets notified when the video size has changed. Mostly designed to update Widget aspect ratio.
|
||||
final Function(Size)? onVideoSizeChanged;
|
||||
|
||||
/// Gets notified when an error occurs
|
||||
final Function(Exception)? onError;
|
||||
|
||||
ApiVideoLiveStreamEventsListener(
|
||||
{this.onConnectionSuccess,
|
||||
this.onConnectionFailed,
|
||||
this.onDisconnection,
|
||||
this.onVideoSizeChanged,
|
||||
this.onError});
|
||||
}
|
||||
|
||||
class ApiVideoLiveStreamWidgetListener {
|
||||
final VoidCallback? onTextureReady;
|
||||
|
||||
ApiVideoLiveStreamWidgetListener({this.onTextureReady});
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'apivideo_live_stream_platform_interface.dart';
|
||||
import 'types.dart';
|
||||
|
||||
/// Controller of the live streaming
|
||||
class ApiVideoMobileLiveStreamPlatform extends ApiVideoLiveStreamPlatform {
|
||||
final MethodChannel _channel =
|
||||
const MethodChannel('video.api.livestream/controller');
|
||||
|
||||
/// Registers this class as the default instance of [PathProviderPlatform].
|
||||
static void registerWith() {
|
||||
ApiVideoLiveStreamPlatform.instance = ApiVideoMobileLiveStreamPlatform();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> initialize() async {
|
||||
final Map<String, dynamic>? reply =
|
||||
await _channel.invokeMapMethod<String, dynamic>('create');
|
||||
return reply!['textureId']! as int;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() {
|
||||
return _channel.invokeMapMethod<String, dynamic>('dispose');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVideoConfig(VideoConfig videoConfig) {
|
||||
return _channel.invokeMethod('setVideoConfig', videoConfig.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAudioConfig(AudioConfig audioConfig) {
|
||||
return _channel.invokeMethod('setAudioConfig', audioConfig.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startStreaming(
|
||||
{required String streamKey, required String url}) {
|
||||
return _channel.invokeMethod('startStreaming', <String, dynamic>{
|
||||
'streamKey': streamKey,
|
||||
'url': url,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopStreaming() {
|
||||
return _channel.invokeMethod('stopStreaming');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startPreview() {
|
||||
return _channel.invokeMethod('startPreview');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopPreview() {
|
||||
return _channel.invokeMethod('stopPreview');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> getIsStreaming() async {
|
||||
final Map<dynamic, dynamic> reply =
|
||||
await _channel.invokeMethod('getIsStreaming') as Map;
|
||||
return reply['isStreaming'] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCameraPosition(CameraPosition cameraPosition) {
|
||||
return _channel.invokeMethod('setCameraPosition',
|
||||
<String, dynamic>{'position': cameraPosition.toJson()});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getBackCameras() async {
|
||||
final List<dynamic> reply =
|
||||
await _channel.invokeMethod('getBackCameras') as List<dynamic>;
|
||||
return reply
|
||||
.whereType<Map<dynamic, dynamic>>()
|
||||
.map((item) => Map<String, dynamic>.from(item))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCameraId(String cameraId) {
|
||||
return _channel.invokeMethod(
|
||||
'setCameraId',
|
||||
<String, dynamic>{'cameraId': cameraId},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CameraPosition> getCameraPosition() async {
|
||||
final Map<dynamic, dynamic> reply =
|
||||
await _channel.invokeMethod('getCameraPosition') as Map;
|
||||
return CameraPosition.fromJson(reply['position'] as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setIsMuted(bool isMuted) {
|
||||
return _channel
|
||||
.invokeMethod('setIsMuted', <String, dynamic>{'isMuted': isMuted});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> getIsMuted() async {
|
||||
final Map<dynamic, dynamic> reply =
|
||||
await _channel.invokeMethod('getIsMuted') as Map;
|
||||
return reply['isMuted'] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Size?> getVideoSize() async {
|
||||
final Map<dynamic, dynamic> reply =
|
||||
await _channel.invokeMethod('getVideoSize') as Map;
|
||||
if (reply.containsKey("width") && reply.containsKey("height")) {
|
||||
return Size(reply["width"] as double, reply["height"] as double);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the preview widget.
|
||||
@override
|
||||
Widget buildPreview(int textureId) {
|
||||
return Texture(textureId: textureId);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<LiveStreamingEvent> liveStreamingEventsFor(int textureId) {
|
||||
return EventChannel('video.api.livestream/events')
|
||||
.receiveBroadcastStream()
|
||||
.map((dynamic map) {
|
||||
final Map<dynamic, dynamic> event = map as Map<dynamic, dynamic>;
|
||||
switch (event['type']) {
|
||||
case 'connected':
|
||||
return LiveStreamingEvent(type: LiveStreamingEventType.connected);
|
||||
case 'disconnected':
|
||||
return LiveStreamingEvent(type: LiveStreamingEventType.disconnected);
|
||||
case 'connectionFailed':
|
||||
return LiveStreamingEvent(
|
||||
type: LiveStreamingEventType.connectionFailed,
|
||||
data: event['message']);
|
||||
case 'videoSizeChanged':
|
||||
return LiveStreamingEvent(
|
||||
type: LiveStreamingEventType.videoSizeChanged,
|
||||
data: Size(event['width'] as double, event['height'] as double));
|
||||
default:
|
||||
return LiveStreamingEvent(type: LiveStreamingEventType.unknown);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
import 'types.dart';
|
||||
|
||||
abstract class ApiVideoLiveStreamPlatform extends PlatformInterface {
|
||||
/// Constructs a ApiVideoPlayerPlatform.
|
||||
ApiVideoLiveStreamPlatform() : super(token: _token);
|
||||
|
||||
static final Object _token = Object();
|
||||
|
||||
static ApiVideoLiveStreamPlatform _instance = _PlatformImplementation();
|
||||
|
||||
/// The default instance of [ApiVideoLiveStreamPlatform] to use.
|
||||
///
|
||||
/// Defaults to [_PlatformImplementation].
|
||||
static ApiVideoLiveStreamPlatform get instance => _instance;
|
||||
|
||||
/// Platform-specific implementations should set this with their own
|
||||
/// platform-specific class that extends [ApiVideoLiveStreamPlatform] when
|
||||
/// they register themselves.
|
||||
static set instance(ApiVideoLiveStreamPlatform instance) {
|
||||
PlatformInterface.verifyToken(instance, _token);
|
||||
_instance = instance;
|
||||
}
|
||||
|
||||
/// Creates a new live stream instance
|
||||
Future<int?> initialize() {
|
||||
throw UnimplementedError('initialize() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Disposes the live stream instance
|
||||
Future<void> dispose() {
|
||||
throw UnimplementedError('dispose() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> setVideoConfig(VideoConfig videoConfig) {
|
||||
throw UnimplementedError('setVideoConfig() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> setAudioConfig(AudioConfig audioConfig) {
|
||||
throw UnimplementedError('setAudioConfig() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> startStreaming(
|
||||
{required String streamKey, required String url}) {
|
||||
throw UnimplementedError('startStreaming() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> stopStreaming() {
|
||||
throw UnimplementedError('stopStreaming() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> startPreview() {
|
||||
throw UnimplementedError('startPreview() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> stopPreview() {
|
||||
throw UnimplementedError('stopPreview() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<bool> getIsStreaming() {
|
||||
throw UnimplementedError('getIsStreaming() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<CameraPosition> getCameraPosition() {
|
||||
throw UnimplementedError('getCameraPosition() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> setCameraPosition(CameraPosition cameraPosition) {
|
||||
throw UnimplementedError('setCameraPosition() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getBackCameras() {
|
||||
throw UnimplementedError('getBackCameras() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> setCameraId(String cameraId) {
|
||||
throw UnimplementedError('setCameraId() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<bool> getIsMuted() {
|
||||
throw UnimplementedError('getIsMuted() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<void> setIsMuted(bool isMuted) {
|
||||
throw UnimplementedError('setIsMuted() has not been implemented.');
|
||||
}
|
||||
|
||||
Future<Size?> getVideoSize() {
|
||||
throw UnimplementedError('getVideoSize() has not been implemented.');
|
||||
}
|
||||
|
||||
/// Returns a Stream of [LiveStreamingEvent]s.
|
||||
Stream<LiveStreamingEvent> liveStreamingEventsFor(int textureId) {
|
||||
throw UnimplementedError(
|
||||
'liveStreamingEventsFor() has not been implemented.');
|
||||
}
|
||||
|
||||
Widget buildPreview(int textureId) {
|
||||
throw UnimplementedError('buildPreview() has not been implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
class _PlatformImplementation extends ApiVideoLiveStreamPlatform {}
|
||||
|
||||
class LiveStreamingEvent {
|
||||
/// Adds optional parameters here if needed
|
||||
final Object? data;
|
||||
|
||||
/// The [LiveStreamingEventType]
|
||||
final LiveStreamingEventType type;
|
||||
|
||||
LiveStreamingEvent({required this.type, this.data});
|
||||
}
|
||||
|
||||
enum LiveStreamingEventType {
|
||||
/// The live streaming is connected.
|
||||
connected,
|
||||
|
||||
/// The live streaming has just been disconnected.
|
||||
disconnected,
|
||||
|
||||
/// The connection to the server failed.
|
||||
connectionFailed,
|
||||
|
||||
/// The video size has changed.
|
||||
videoSizeChanged,
|
||||
|
||||
/// Unknown event
|
||||
unknown
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export 'types/audio_config.dart';
|
||||
export 'types/camera_position.dart';
|
||||
export 'types/channel.dart';
|
||||
export 'types/resolution.dart';
|
||||
export 'types/sample_rate.dart';
|
||||
export 'types/video_config.dart';
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'channel.dart';
|
||||
import 'sample_rate.dart';
|
||||
|
||||
part 'audio_config.g.dart';
|
||||
|
||||
/// Live streaming audio configuration.
|
||||
@JsonSerializable()
|
||||
class AudioConfig {
|
||||
/// The video bitrate in bps
|
||||
int bitrate;
|
||||
|
||||
/// The number of audio channels
|
||||
/// Only available on Android
|
||||
Channel channel;
|
||||
|
||||
/// The sample rate of the audio capture
|
||||
/// Only available on Android
|
||||
SampleRate sampleRate;
|
||||
|
||||
/// Enable the echo cancellation
|
||||
/// Only available on Android
|
||||
bool enableEchoCanceler;
|
||||
|
||||
/// Enable the noise suppressor
|
||||
/// Only available on Android
|
||||
bool enableNoiseSuppressor;
|
||||
|
||||
/// Creates a new [AudioConfig] instance.
|
||||
///
|
||||
/// [sampleRate] is only supported on Android.
|
||||
/// [channel] is only supported on Android.
|
||||
AudioConfig(
|
||||
{this.bitrate = 128000,
|
||||
this.channel = Channel.stereo,
|
||||
this.sampleRate = SampleRate.kHz_44_1,
|
||||
this.enableEchoCanceler = true,
|
||||
this.enableNoiseSuppressor = true})
|
||||
: assert(bitrate > 0);
|
||||
|
||||
/// Creates a [AudioConfig] from a [json] map.
|
||||
factory AudioConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$AudioConfigFromJson(json);
|
||||
|
||||
/// Creates a json map from a [AudioConfig].
|
||||
Map<String, dynamic> toJson() => _$AudioConfigToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'audio_config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AudioConfig _$AudioConfigFromJson(Map<String, dynamic> json) => AudioConfig(
|
||||
bitrate: json['bitrate'] as int? ?? 128000,
|
||||
channel: $enumDecodeNullable(_$ChannelEnumMap, json['channel']) ??
|
||||
Channel.stereo,
|
||||
sampleRate:
|
||||
$enumDecodeNullable(_$SampleRateEnumMap, json['sampleRate']) ??
|
||||
SampleRate.kHz_44_1,
|
||||
enableEchoCanceler: json['enableEchoCanceler'] as bool? ?? true,
|
||||
enableNoiseSuppressor: json['enableNoiseSuppressor'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AudioConfigToJson(AudioConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'bitrate': instance.bitrate,
|
||||
'channel': _$ChannelEnumMap[instance.channel]!,
|
||||
'sampleRate': _$SampleRateEnumMap[instance.sampleRate]!,
|
||||
'enableEchoCanceler': instance.enableEchoCanceler,
|
||||
'enableNoiseSuppressor': instance.enableNoiseSuppressor,
|
||||
};
|
||||
|
||||
const _$ChannelEnumMap = {
|
||||
Channel.stereo: 'stereo',
|
||||
Channel.mono: 'mono',
|
||||
};
|
||||
|
||||
const _$SampleRateEnumMap = {
|
||||
SampleRate.kHz_11: 11025,
|
||||
SampleRate.kHz_22: 22050,
|
||||
SampleRate.kHz_44_1: 44100,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/// Camera facing direction
|
||||
enum CameraPosition {
|
||||
/// Front camera
|
||||
front,
|
||||
|
||||
/// Back camera
|
||||
back,
|
||||
|
||||
/// Other camera (external for example)
|
||||
other;
|
||||
|
||||
String toJson() => name;
|
||||
|
||||
static CameraPosition fromJson(String json) => values.byName(json);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// Audio channel
|
||||
enum Channel {
|
||||
/// Stereo (2 channels)
|
||||
@JsonValue("stereo")
|
||||
stereo,
|
||||
|
||||
/// Mono (1 channel)
|
||||
@JsonValue("mono")
|
||||
mono,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// Enumeration for camera resolution
|
||||
/// Only 16/9 resolutions are supported.
|
||||
enum Resolution {
|
||||
/// 426x240
|
||||
@JsonValue("240p")
|
||||
RESOLUTION_240(size: Size(426, 240)),
|
||||
|
||||
/// 640x360
|
||||
@JsonValue("360p")
|
||||
RESOLUTION_360(size: Size(640, 360)),
|
||||
|
||||
/// 854x480
|
||||
@JsonValue("480p")
|
||||
RESOLUTION_480(size: Size(854, 480)),
|
||||
|
||||
/// 1280x720
|
||||
@JsonValue("720p")
|
||||
RESOLUTION_720(size: Size(1280, 720)),
|
||||
|
||||
/// 1920x1080
|
||||
@JsonValue("1080p")
|
||||
RESOLUTION_1080(size: Size(1920, 1080));
|
||||
|
||||
const Resolution({required this.size});
|
||||
|
||||
final Size size;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// Enumeration for supported RTMP sample rate
|
||||
@JsonEnum(valueField: 'value')
|
||||
enum SampleRate {
|
||||
/// 11025 Hz
|
||||
kHz_11(value: 11025),
|
||||
|
||||
/// 22050 Hz
|
||||
kHz_22(value: 22050),
|
||||
|
||||
/// 44100 Hz
|
||||
kHz_44_1(value: 44100);
|
||||
|
||||
const SampleRate({required this.value});
|
||||
|
||||
final int value;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'resolution.dart';
|
||||
|
||||
part 'video_config.g.dart';
|
||||
|
||||
/// Live streaming video configuration.
|
||||
@JsonSerializable()
|
||||
class VideoConfig {
|
||||
/// The video bitrate in bps
|
||||
int bitrate;
|
||||
|
||||
/// The live streaming video resolution
|
||||
Resolution resolution;
|
||||
|
||||
/// The video frame rate in fps
|
||||
int fps;
|
||||
|
||||
/// Creates a [VideoConfig] instance
|
||||
VideoConfig(
|
||||
{required this.bitrate,
|
||||
this.resolution = Resolution.RESOLUTION_720,
|
||||
this.fps = 30})
|
||||
: assert(bitrate > 0),
|
||||
assert(fps > 0);
|
||||
|
||||
/// Creates a [VideoConfig] instance where bitrate is set according to the given [resolution].
|
||||
VideoConfig.withDefaultBitrate(
|
||||
{this.resolution = Resolution.RESOLUTION_720, this.fps = 30})
|
||||
: assert(fps > 0),
|
||||
bitrate = _getDefaultBitrate(resolution);
|
||||
|
||||
/// Creates a [VideoConfig] from a [json] map.
|
||||
factory VideoConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$VideoConfigFromJson(json);
|
||||
|
||||
/// Creates a json map from a [VideoConfig].
|
||||
Map<String, dynamic> toJson() => _$VideoConfigToJson(this);
|
||||
|
||||
/// Returns the default bitrate for the given [resolution].
|
||||
static int _getDefaultBitrate(Resolution resolution) {
|
||||
switch (resolution) {
|
||||
case Resolution.RESOLUTION_240:
|
||||
return 800000;
|
||||
case Resolution.RESOLUTION_360:
|
||||
return 1000000;
|
||||
case Resolution.RESOLUTION_480:
|
||||
return 1300000;
|
||||
case Resolution.RESOLUTION_720:
|
||||
return 2000000;
|
||||
case Resolution.RESOLUTION_1080:
|
||||
return 3500000;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'video_config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
VideoConfig _$VideoConfigFromJson(Map<String, dynamic> json) => VideoConfig(
|
||||
bitrate: json['bitrate'] as int,
|
||||
resolution:
|
||||
$enumDecodeNullable(_$ResolutionEnumMap, json['resolution']) ??
|
||||
Resolution.RESOLUTION_720,
|
||||
fps: json['fps'] as int? ?? 30,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VideoConfigToJson(VideoConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'bitrate': instance.bitrate,
|
||||
'resolution': _$ResolutionEnumMap[instance.resolution]!,
|
||||
'fps': instance.fps,
|
||||
};
|
||||
|
||||
const _$ResolutionEnumMap = {
|
||||
Resolution.RESOLUTION_240: '240p',
|
||||
Resolution.RESOLUTION_360: '360p',
|
||||
Resolution.RESOLUTION_480: '480p',
|
||||
Resolution.RESOLUTION_720: '720p',
|
||||
Resolution.RESOLUTION_1080: '1080p',
|
||||
};
|
||||
Reference in New Issue
Block a user