@@ -2,199 +2,218 @@ import 'dart:async';
2
2
import 'dart:html' as html;
3
3
4
4
import 'package:flutter/material.dart' ;
5
+ import 'package:flutter/services.dart' ;
5
6
6
7
import '../enums.dart' ;
7
8
import './ui_fake.dart' if (dart.library.html) 'dart:ui' as ui;
8
9
import 'media_stream.dart' ;
9
10
10
- typedef VideoSizeChangeCallback = void Function (
11
- int textureId, double width, double height);
12
- typedef StateChangeCallback = void Function ();
13
- typedef FirstFrameRenderedCallback = void Function ();
14
-
15
- class RTCVideoRenderer {
16
- RTCVideoRenderer ();
17
- var _width = 0.0 , _height = 0.0 ;
18
- var _isFirstFrameRendered = false ;
19
- MediaStream _srcObject;
20
- VideoSizeChangeCallback onVideoSizeChanged;
21
- StateChangeCallback onStateChanged;
22
- FirstFrameRenderedCallback onFirstFrameRendered;
23
-
24
- HtmlElementView _htmlElementView;
25
- html.VideoElement _htmlVideoElement;
26
-
27
- final _videoViews = < html.VideoElement > [];
11
+ // An error code value to error name Map.
12
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
13
+ const Map <int , String > _kErrorValueToErrorName = {
14
+ 1 : 'MEDIA_ERR_ABORTED' ,
15
+ 2 : 'MEDIA_ERR_NETWORK' ,
16
+ 3 : 'MEDIA_ERR_DECODE' ,
17
+ 4 : 'MEDIA_ERR_SRC_NOT_SUPPORTED' ,
18
+ };
19
+
20
+ // An error code value to description Map.
21
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
22
+ const Map <int , String > _kErrorValueToErrorDescription = {
23
+ 1 : 'The user canceled the fetching of the video.' ,
24
+ 2 : 'A network error occurred while fetching the video, despite having previously been available.' ,
25
+ 3 : 'An error occurred while trying to decode the video, despite having previously been determined to be usable.' ,
26
+ 4 : 'The video has been found to be unsuitable (missing or in a format not supported by your browser).' ,
27
+ };
28
+
29
+ // The default error message, when the error is an empty string
30
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message
31
+ const String _kDefaultErrorMessage =
32
+ 'No further diagnostic information can be determined or provided.' ;
33
+
34
+ @immutable
35
+ class RTCVideoValue {
36
+ const RTCVideoValue ({
37
+ this .width = 0.0 ,
38
+ this .height = 0.0 ,
39
+ this .rotation = 0 ,
40
+ this .renderVideo = false ,
41
+ });
42
+ static const RTCVideoValue empty = RTCVideoValue ();
43
+ final double width;
44
+ final double height;
45
+ final int rotation;
46
+ final bool renderVideo;
47
+ double get aspectRatio {
48
+ if (width == 0.0 || height == 0.0 ) {
49
+ return 1.0 ;
50
+ }
51
+ return (rotation == 90 || rotation == 270 )
52
+ ? height / width
53
+ : width / height;
54
+ }
28
55
29
- bool get isMuted => _htmlVideoElement? .muted ?? true ;
30
- set isMuted (bool i) => _htmlVideoElement? .muted = i;
56
+ RTCVideoValue copyWith ({
57
+ double width,
58
+ double height,
59
+ int rotation,
60
+ bool renderVideo,
61
+ }) {
62
+ return RTCVideoValue (
63
+ width: width ?? this .width,
64
+ height: height ?? this .height,
A3E2
65
+ rotation: rotation ?? this .rotation,
66
+ renderVideo: (this .width != 0 && this .height != 0 && renderVideo) ??
67
+ this .renderVideo,
68
+ );
69
+ }
31
70
32
- HtmlElementView get htmlElementView => _htmlElementView;
71
+ @override
72
+ String toString () =>
73
+ '$runtimeType (width: $width , height: $height , rotation: $rotation )' ;
74
+ }
33
75
34
- void fixVideoElements () => _videoViews.forEach ((v) => v.play ());
76
+ class RTCVideoRenderer extends ValueNotifier <RTCVideoValue > {
77
+ RTCVideoRenderer ()
78
+ : textureId = _textureCounter++ ,
79
+ super (RTCVideoValue .empty);
35
80
36
- /// You don\'t have to call RTCVideoRenderer.initialize if you use only Flutter web
37
- void initialize () async {}
81
+ static int _textureCounter = 1 ;
82
+ final int textureId;
83
+ html.VideoElement videoElement;
84
+ MediaStream _srcObject;
38
85
39
- int get rotation => 0 ;
86
+ bool get muted => videoElement ? .muted ?? true ;
40
87
41
- double get width => _width ?? 1080 ;
88
+ set muted ( bool mute) => videoElement ? .muted = mute ;
42
89
43
- double get height => _height ?? 1920 ;
90
+ bool get renderVideo => videoElement != null && srcObject != null ;
44
91
45
- int get textureId => 0 ;
92
+ Future <void > initialize () async {
93
+ videoElement = html.VideoElement ()
94
+ //..src = 'https://flutter-webrtc-video-view-RTCVideoRenderer-$textureId'
95
+ ..autoplay = true
96
+ ..controls = false
97
+ ..style.objectFit = 'contain' // contain or cover
98
+ ..style.border = 'none' ;
46
99
47
- double get aspectRatio =>
48
- (_width == 0 || _height == 0 ) ? ( 9 / 16 ) : _width / _height ;
100
+ // Allows Safari iOS to play the video inline
101
+ videoElement. setAttribute ( 'playsinline' , 'true' ) ;
49
102
50
- MediaStream get srcObject => _srcObject;
103
+ // ignore: undefined_prefixed_name
104
+ ui.platformViewRegistry.registerViewFactory (
105
+ 'RTCVideoRenderer-$textureId ' , (int viewId) => videoElement);
51
106
52
- set srcObject (MediaStream stream) {
53
- if (stream == null ) {
54
- return ;
55
- }
56
-
57
- _srcObject = stream;
58
- if (_htmlElementView != null ) {
59
- findHtmlView ()? .srcObject = stream? .jsStream;
60
- }
61
-
62
- ui.platformViewRegistry.registerViewFactory (stream.id, (int viewId) {
63
- final x = html.VideoElement ();
64
- x.autoplay = true ;
65
- x.muted = _srcObject.ownerTag == 'local' ;
66
- x.srcObject = stream.jsStream;
67
- x.id = stream.id;
68
- _htmlVideoElement = x;
69
- _videoViews.add (x);
70
- return x;
107
+ videoElement.onCanPlay.listen ((dynamic _) {
108
+ value = value.copyWith (
109
+ rotation: 0 ,
110
+ width: videoElement.videoWidth.toDouble () ?? 0.0 ,
111
+ height: videoElement.videoHeight.toDouble () ?? 0.0 ,
112
+ renderVideo: renderVideo);
113
+ print ('RTCVideoRenderer: videoElement.onCanPlay ${value .toString ()}' );
71
114
});
72
115
73
- _htmlElementView = HtmlElementView (viewType: stream.id);
74
- onStateChanged? .call ();
75
- }
76
-
77
- void findAndApply (Size size) {
78
- final htmlView = findHtmlView ();
79
- if (_srcObject == null || htmlView == null ) return ;
80
- if (htmlView.width == size.width.toInt () &&
81
- htmlView.height == size.height.toInt ()) return ;
82
-
83
- htmlView.srcObject = _srcObject.jsStream;
84
- htmlView.width = size.width.toInt ();
85
- htmlView.height = size.height.toInt ();
116
+ videoElement.onResize.listen ((dynamic _) {
117
+ value = value.copyWith (
118
+ rotation: 0 ,
119
+ width: videoElement.videoWidth.toDouble () ?? 0.0 ,
120
+ height: videoElement.videoHeight.toDouble () ?? 0.0 ,
121
+ renderVideo: renderVideo);
122
+ print ('RTCVideoRenderer: videoElement.onResize ${value .toString ()}' );
123
+ });
86
124
87
- htmlView.onLoadedMetadata.listen ((_) {
88
- _checkVideoSizeChanged (htmlView);
125
+ // The error event fires when some form of error occurs while attempting to load or perform the media.
126
+ videoElement.onError.listen ((html.Event _) {
127
+ // The Event itself (_) doesn't contain info about the actual error.
128
+ // We need to look at the HTMLMediaElement.error.
129
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
130
+ var error = videoElement.error;
131
+ throw PlatformException (
132
+ code: _kErrorValueToErrorName[error.code],
133
+ message: error.message != '' ? error.message : _kDefaultErrorMessage,
134
+ details: _kErrorValueToErrorDescription[error.code],
135
+ );
136
+ });
89
137
90
- if (! _isFirstFrameRendered) {
91
- onFirstFrameRendered? .call ();
92
- _isFirstFrameRendered = true ;
93
- }
138
+ videoElement.onEnded.listen ((dynamic _) {
139
+ print ('RTCVideoRenderer: videoElement.onEnded' );
94
140
});
141
+ }
95
142
96
- htmlView.onResize. listen ((_) => _checkVideoSizeChanged (htmlView)) ;
143
+ MediaStream get srcObject => _srcObject ;
97
144
98
- _checkVideoSizeChanged (htmlView);
99
- }
145
+ set srcObject ( MediaStream stream) {
146
+ if (videoElement == null ) throw 'Call initialize before setting the stream' ;
100
147
101
- void _checkVideoSizeChanged (html.VideoElement htmlView) {
102
- if (htmlView.videoWidth != 0 &&
103
- htmlView.videoHeight != 0 &&
104
- (_width != htmlView.videoWidth || _height != htmlView.videoHeight)) {
105
- _width = htmlView.videoWidth.toDouble ();
106
- _height = htmlView.videoHeight.toDouble ();
107
- onVideoSizeChanged? .call (0 , _width, _height);
148
+ if (stream == null ) {
149
+ videoElement.srcObject = null ;
150
+ _srcObject = null ;
151
+ return ;
108
152
}
153
+ _srcObject = stream;
154
+ videoElement.srcObject = stream? .jsStream;
155
+ videoElement.muted = stream? .ownerTag == 'local' ;
156
+ value = value.copyWith (renderVideo: renderVideo);
109
157
}
110
158
111
- html.VideoElement findHtmlView () {
112
- if (_htmlVideoElement != null ) return _htmlVideoElement;
113
- final fltPv = html.document.getElementsByTagName ('flt-platform-view' );
114
- if (fltPv.isEmpty) return null ;
115
-
179B
final lastChild = (fltPv.first as html.Element ).shadowRoot.lastChild;
116
- if (! (lastChild is html.VideoElement )) return null ;
117
- final videoElement = lastChild as html.VideoElement ;
118
- if (_srcObject != null && videoElement.id != _srcObject.id) return null ;
119
- return lastChild;
120
- }
121
-
122
- ///By calling the dispose you are safely disposing the MediaStream
159
+ @override
123
160
Future <void > dispose () async {
161
+ super .dispose ();
124
162
await _srcObject? .dispose ();
125
-
126
163
_srcObject = null ;
127
- findHtmlView ()? .srcObject = null ;
128
- _videoViews.forEach ((element) {
129
- element.srcObject = null ;
130
- });
131
- // TODO(cloudwebrtc): ???
132
- // https://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element/28060352
164
+ videoElement.removeAttribute ('src' );
165
+ videoElement.load ();
133
166
}
134
167
}
135
168
136
169
class RTCVideoView extends StatefulWidget {
137
- RTCVideoView (this ._renderer,
138
- {Key key,
139
- this .objectFit = RTCVideoViewObjectFit .RTCVideoViewObjectFitContain ,
140
- this .mirror = false })
141
- : assert (objectFit != null ),
170
+ RTCVideoView (
171
+ this ._renderer, {
172
+ Key key,
173
+ this .objectFit = RTCVideoViewObjectFit .RTCVideoViewObjectFitContain ,
174
+ this .mirror = false ,
175
+ }) : assert (objectFit != null ),
142
176
assert (mirror != null ),
143
177
super (key: key);
144
178
145
179
final RTCVideoRenderer _renderer;
146
180
final RTCVideoViewObjectFit objectFit;
147
- final mirror;
148
-
181
+ final bool mirror;
149
182
@override
150
- _RTCVideoViewState createState () => _RTCVideoViewState (_renderer );
183
+ _RTCVideoViewState createState () => _RTCVideoViewState ();
151
184
}
152
185
153
186
class _RTCVideoViewState extends State <RTCVideoView > {
154
- _RTCVideoViewState (this ._renderer);
155
-
156
- final RTCVideoRenderer _renderer;
157
- double _aspectRatio;
187
+ _RTCVideoViewState ();
158
188
159
189
@override
160
190
void initState () {
161
191
super .initState ();
162
- _setCallbacks ();
163
- _aspectRatio = _renderer.aspectRatio;
164
- }
165
-
166
- @override
167
- void dispose () {
168
- super .dispose ();
169
- _renderer.onStateChanged = null ;
192
+ widget._renderer? .addListener (() => setState (() {}));
170
193
}
171
194
172
- void _setCallbacks () {
173
- _renderer.onStateChanged = () {
174
- setState (() {
175
- _aspectRatio = _renderer.aspectRatio;
176
- });
177
- };
178
- }
179
-
180
- Widget _buildVideoView (BoxConstraints constraints) {
181
- _renderer.findAndApply (constraints.biggest);
182
- return Container (
183
- width: constraints.maxWidth,
184
- height: constraints.maxHeight,
185
- child: SizedBox (
186
- width: constraints.maxHeight * _aspectRatio,
187
- height: constraints.maxHeight,
188
- child: _renderer.htmlElementView ?? Container ()));
195
+ Widget buildVideoElementView (RTCVideoViewObjectFit objFit, bool mirror) {
196
+ // TODO(cloudwebrtc): Add css style for mirror.
197
+ widget._renderer.videoElement.style.objectFit =
198
+ objFit == RTCVideoViewObjectFit .RTCVideoViewObjectFitContain
199
+ ? 'contain'
200
+ : 'cover' ;
201
+ return HtmlElementView (
202
+ viewType: 'RTCVideoRenderer-${widget ._renderer .textureId }' );
189
203
}
190
204
191
205
@override
192
206
Widget build (BuildContext context) {
193
- var renderVideo = _renderer._srcObject != null ;
194
207
return LayoutBuilder (
195
208
builder: (BuildContext context, BoxConstraints constraints) {
196
209
return Center (
197
- child: renderVideo ? _buildVideoView (constraints) : Container ());
210
+ child: Container (
211
+ width: constraints.maxWidth,
212
+ height: constraints.maxHeight,
213
+ child: widget._renderer.renderVideo
214
+ ? buildVideoElementView (widget.objectFit, widget.mirror)
215
+ : Container (),
216
+ ));
198
217
});
199
218
}
200
219
}
0 commit comments