@@ -2,205 +2,216 @@ 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 VideoRotationChangeCallback = void Function (
11
- int textureId, int rotation);
12
- typedef VideoSizeChangeCallback = void Function (
13
- int textureId, double width, double height);
14
-
15
- class RTCVideoRenderer {
16
- RTCVideoRenderer ();
17
- double _width = 0.0 , _height = 0.0 ;
18
- MediaStream _srcObject;
19
- VideoSizeChangeCallback onVideoSizeChanged;
20
- VideoRotationChangeCallback onVideoRotationChanged;
21
- dynamic onFirstFrameRendered;
22
- var isFirstFrameRendered = false ;
23
- dynamic onStateChanged;
24
- HtmlElementView htmlElementView;
25
- html.VideoElement _htmlVideoElement;
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
+ }
26
55
27
- static final _videoViews = < html.VideoElement > [];
56
+ RTCVideoValue copyWith ({
57
+ double width,
58
+ double height,
59
+ int rotation,
60
+ bool renderVideo,
61
+ }) {
62
+ return RTCVideoVa
9E81
lue (
63
+ width: width ?? this .width,
64
+ height: height ?? this .height,
65
+ rotation: rotation ?? this .rotation,
66
+ renderVideo: (this .width != 0 && this .height != 0 && renderVideo) ??
67
+ this .renderVideo,
68
+ );
69
+ }
28
70
29
- bool get isMuted => _htmlVideoElement? .muted ?? true ;
30
- set isMuted (bool i) => _htmlVideoElement? .muted = i;
71
+ @override
72
+ String toString () =>
73
+ '$runtimeType (width: $width , height: $height , rotation: $rotation )' ;
74
+ }
31
75
32
- static void fixVideoElements () => _videoViews.forEach ((v) => v.play ());
76
+ class RTCVideoRenderer extends ValueNotifier <RTCVideoValue > {
77
+ RTCVideoRenderer ()
78
+ : textureId = _textureCounter++ ,
79
+ super (RTCVideoValue .empty);
33
80
34
- void initialize () async {
35
- print ('You don\' t have to call RTCVideoRenderer.initialize on Flutter Web' );
36
- }
81
+ static int _textureCounter = 1 ;
82
+ final int textureId;
83
+ html.VideoElement videoElement;
84
+ bool isInitialized = false ;
37
85
38
- int get rotation => 0 ;
86
+ MediaStream _srcObject ;
39
87
40
- double get width => _width ?? 1080 ;
88
+ bool get muted => videoElement ? .muted ?? true ;
41
89
42
- double get height => _height ?? 1920 ;
90
+ set muted ( bool mute) => videoElement ? .muted = mute ;
43
91
44
- int get textureId => 0 ;
92
+ bool get renderVideo => srcObject != null ;
45
93
46
- double get aspectRatio =>
47
- (_width == 0 || _height == 0 ) ? (9 / 16 ) : _width / _height;
94
+ void initialize () {
95
+ videoElement = html.VideoElement ()
96
+ //..src = 'https://flutter-webrtc-video-view-RTCVideoRenderer-$textureId'
97
+ ..autoplay = false
98
+ ..controls = false
99
+ ..style.objectFit = 'contain' // contain or cover
100
+ ..style.border = 'none' ;
48
101
49
- MediaStream get srcObject => _srcObject;
102
+ // Allows Safari iOS to play the video inline
103
+ videoElement.setAttribute ('playsinline' , 'true' );
50
104
51
- set srcObject (MediaStream stream) {
52
- _srcObject = stream;
105
+ // ignore: undefined_prefixed_name
106
+ ui.platformViewRegistry.registerViewFactory (
107
+ 'RTCVideoRenderer-$textureId ' , (int viewId) => videoElement);
53
108
54
- if (_srcObject == null ) {
55
- findHtmlView ()? .srcObject = null ;
56
- return ;
57
- }
109
+ videoElement.onCanPlay.listen ((dynamic _) {
110
+ if (! isInitialized) {
111
+ isInitialized = true ;
112
+ value = value.copyWith (
113
+ rotation: 0 ,
114
+ width: videoElement.videoWidth.toDouble () ?? 0.0 ,
115
+ height: videoElement.videoHeight.toDouble () ?? 0.0 ,
116
+ renderVideo: renderVideo);
117
+ }
118
+ print ('RTCVideoRenderer: videoElement.onCanPlay' );
119
+ });
58
120
59
- if (htmlElementView != null ) {
60
- findHtmlView ()? .srcObject = stream? .jsStream;
61
- }
121
+ // The error event fires when some form of error occurs while attempting to load or perform the media.
122
+ videoElement.onError.listen ((html.Event _) {
123
+ // The Event itself (_) doesn't contain info about the actual error.
124
+ // We need to look at the HTMLMediaElement.error.
125
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
126
+ var error = videoElement.error;
127
+ throw PlatformException (
128
+ code: _kErrorValueToErrorName[error.code],
129
+ message: error.message != '' ? error.message : _kDefaultErrorMessage,
130
+ details: _kErrorValueToErrorDescription[error.code],
131
+ );
132
+ });
62
133
63
- ui.platformViewRegistry.registerViewFactory (stream.id, (int viewId) {
64
- final x = html.VideoElement ();
65
- x.autoplay = true ;
66
- x.muted = _srcObject.ownerTag == 'local' ;
67
- x.srcObject = stream.jsStream;
68
- x.id = stream.id;
69
- _htmlVideoElement = x;
70
- _videoViews.add (x);
71
- return x;
134
+ videoElement.onEnded.listen ((dynamic _) {
135
+ print ('RTCVideoRenderer: videoElement.onEnded' );
72
136
});
73
- htmlElementView = HtmlElementView (viewType: stream.id);
74
- if (onStateChanged != null ) {
75
- onStateChanged ();
76
- }
77
137
}
78
138
79
- void findAndApply (Size size) {
80
- final htmlView = findHtmlView ();
81
- if (_srcObject != null && htmlView != null ) {
82
- if (htmlView.width == size.width.toInt () &&
83
- htmlView.height == size.height.toInt ()) return ;
84
- htmlView.srcObject = _srcObject.jsStream;
85
- htmlView.width = size.width.toInt ();
86
- htmlView.height = size.height.toInt ();
87
- htmlView.onLoadedMetadata.listen ((_) {
88
- if (htmlView.videoWidth != 0 &&
89
- htmlView.videoHeight != 0 &&
90
- (_width != htmlView.videoWidth ||
91
- _height != htmlView.videoHeight)) {
92
- _width = htmlView.videoWidth.toDouble ();
93
- _height = htmlView.videoHeight.toDouble ();
94
- if (onVideoSizeChanged != null ) {
95
- onVideoSizeChanged (0 , _width, _height);
96
- }
97
- }
98
- if (! isFirstFrameRendered && onFirstFrameRendered != null ) {
99
- onFirstFrameRendered ();
100
- isFirstFrameRendered = true ;
101
- }
102
- });
103
- htmlView.onResize.listen ((_) {
104
- if (htmlView.videoWidth != 0 &&
105
- htmlView.videoHeight != 0 &&
106
- (_width != htmlView.videoWidth ||
107
- _height != htmlView.videoHeight)) {
108
- _width = htmlView.videoWidth.toDouble ();
109
- _height = htmlView.videoHeight.toDouble ();
110
- if (onVideoSizeChanged != null ) {
111
- onVideoSizeChanged (0 , _width, _height);
112
- }
113
- }
114
- });
115
- if (htmlView.videoWidth != 0 &&
116
- htmlView.videoHeight != 0 &&
117
- (_width != htmlView.videoWidth || _height != htmlView.videoHeight)) {
118
- _width = htmlView.videoWidth.toDouble ();
119
- _height = htmlView.videoHeight.toDouble ();
120
- if (onVideoSizeChanged != null ) onVideoSizeChanged (0 , _width, _height);
121
- }
139
+ MediaStream get srcObject => _srcObject;
140
+
141
+ set srcObject (MediaStream stream) {
142
+ if (stream == null ) {
143
+ videoElement.srcObject = null ;
144
+ _srcObject = null ;
145
+ return ;
122
146
}
147
+ _srcObject = stream;
148
+ videoElement.srcObject = stream? .jsStream;
149
+ videoElement.muted = stream? .ownerTag == 'local' ;
150
+ videoElement.play ().catchError ((e) {
151
+ // play() attempts to begin playback of the media. It returns
152
+ // a Promise which can get rejected in case of failure to begin
153
+ // playback for any reason, such as permission issues.
154
+ // The rejection handler is called with a DomException.
155
+ // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play
156
+ html.DomException exception = e;
157
+ throw PlatformException (
158
+ code: exception.name,
159
+ message: exception.message,
160
+ );
161
+ }, test: (e) => e is html.DomException );
162
+ value = value.copyWith (renderVideo: renderVideo);
123
163
}
124
164
125
- html.VideoElement findHtmlView () {
126
- if (_htmlVideoElement != null ) return _htmlVideoElement;
127
- final fltPv = html.document.getElementsByTagName ('flt-platform-view' );
128
- if (fltPv.isEmpty) return null ;
129
- final lastChild = (fltPv.first as html.Element ).shadowRoot.lastChild;
130
- if (! (lastChild is html.VideoElement )) return null ;
131
- final videoElement = lastChild as html.VideoElement ;
132
- if (_srcObject != null && videoElement.id != _srcObject.id) return null ;
133
- return lastChild;
165
+ Widget buildVideoElementView (RTCVideoViewObjectFit objFit, bool mirror) {
166
+ // TODO(cloudwebrtc): Add css style for mirror.
167
+ videoElement.style.objectFit =
168
+ objFit == RTCVideoViewObjectFit .RTCVideoViewObjectFitContain
169
+ ? 'contain'
170
+ : 'cover' ;
171
+ return HtmlElementView (viewType: 'RTCVideoRenderer-$textureId ' );
134
172
}
135
173
136
- Future <Null > dispose () async {
137
- // TODO(cloudwebrtc): ???
138
- // https://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element/28060352
174
+ @override
175
+ Future <void > dispose () async {
10000
176
+ super .dispose ();
177
+ await _srcObject? .dispose ();
178
+ _srcObject = null ;
179
+ videoElement.removeAttribute ('src' );
180
+ videoElement.load ();
139
181
}
140
182
}
141
183
142
- class RTCVideoView extends StatefulWidget {
143
- RTCVideoView (this ._renderer,
144
- {Key key,
145
- this .objectFit = RTCVideoViewObjectFit .RTCVideoViewObjectFitContain ,
146
- this .mirror = false })
147
- : assert (objectFit != null ),
184
+ class RTCVideoView extends StatelessWidget {
185
+ RTCVideoView (
186
+ this ._renderer, {
187
+ Key key,
188
+ this .objectFit = RTCVideoViewObjectFit .RTCVideoViewObjectFitContain ,
189
+ this .mirror = false ,
190
+ }) : assert (objectFit != null ),
148
191
assert (mirror != null ),
149
192
super (key: key);
150
193
151
194
final RTCVideoRenderer _renderer;
152
195
final RTCVideoViewObjectFit objectFit;
153
- final mirror;
154
-
155
- @override
156
- _RTCVideoViewState createState () => _RTCVideoViewState (_renderer);
157
- }
158
-
159
- class _RTCVideoViewState extends State <RTCVideoView > {
160
- _RTCVideoViewState (this ._renderer);
161
-
162
- final RTCVideoRenderer _renderer;
163
- double _aspectRatio;
164
-
165
- @override
166
- void initState () {
167
- super .initState ();
168
- _setCallbacks ();
169
- _aspectRatio = _renderer.aspectRatio;
170
- }
196
+ final bool mirror;
171
197
172
198
@override
173
- void dispose () {
174
- super .dispose ();
175
- _renderer.onStateChanged = null ;
176
- }
177
-
178
- void _setCallbacks () {
179
- _renderer.onStateChanged = () {
180
- setState (() {
181
- _aspectRatio = _renderer.aspectRatio;
182
- });
183
- };
199
+ Widget build (BuildContext context) {
200
+ return LayoutBuilder (
201
+ builder: (BuildContext context, BoxConstraints constraints) =>
202
+ _buildVideoView (constraints));
184
203
}
185
204
186
205
Widget _buildVideoView (BoxConstraints constraints) {
187
- _renderer.findAndApply (constraints.biggest);
188
- return Container (
189
- width: constraints.maxWidth,
190
- height: constraints.maxHeight,
191
- child: SizedBox (
192
- width: constraints.maxHeight * _aspectRatio,
193
- height: constraints.maxHeight,
194
- child: _renderer.htmlElementView ?? Container ()));
195
- }
196
-
197
- @override
198
- Widget build (BuildContext context) {
199
- var renderVideo = _renderer._srcObject != null ;
200
- return LayoutBuilder (
201
- builder: (BuildContext context, BoxConstraints constraints) {
202
- return Center (
203
- child: renderVideo ? _buildVideoView (constraints) : Container ());
204
- });
206
+ return Center (
207
+ child: Container (
208
+ width: constraints.maxWidth,
209
+ height: constraints.maxHeight,
210
+ child: Center (
211
+ child: _renderer.srcObject != null
212
+ ? _renderer.buildVideoElementView (objectFit, mirror)
213
+ : Container (),
214
+ ),
215
+ ));
205
216
}
206
217
}
0 commit comments