10000 Merge branch 'master' into cleanup · tmthecoder/flutter-webrtc@5a04d85 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5a04d85

Browse files
authored
Merge branch 'master' into cleanup
2 parents 91f76bf + e771b0d commit 5a04d85

File tree

1 file changed

+156
-137
lines changed

1 file changed

+156
-137
lines changed

lib/src/web/rtc_video_view.dart

Lines changed: 156 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -2,199 +2,218 @@ import 'dart:async';
22
import 'dart:html' as html;
33

44
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
56

67
import '../enums.dart';
78
import './ui_fake.dart' if (dart.library.html) 'dart:ui' as ui;
89
import 'media_stream.dart';
910

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+
}
2855

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+
}
3170

32-
HtmlElementView get htmlElementView => _htmlElementView;
71+
@override
72+
String toString() =>
73+
'$runtimeType(width: $width, height: $height, rotation: $rotation)';
74+
}
3375

34-
void fixVideoElements() => _videoViews.forEach((v) => v.play());
76+
class RTCVideoRenderer extends ValueNotifier<RTCVideoValue> {
77+
RTCVideoRenderer()
78+
: textureId = _textureCounter++,
79+
super(RTCVideoValue.empty);
3580

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;
3885

39-
int get rotation => 0;
86+
bool get muted => videoElement?.muted ?? true;
4087

41-
double get width => _width ?? 1080;
88+
set muted(bool mute) => videoElement?.muted = mute;
4289

43-
double get height => _height ?? 1920;
90+
bool get renderVideo => videoElement != null && srcObject != null;
4491

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';
4699

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');
49102

50-
MediaStream get srcObject => _srcObject;
103+
// ignore: undefined_prefixed_name
104+
ui.platformViewRegistry.registerViewFactory(
105+
'RTCVideoRenderer-$textureId', (int viewId) => videoElement);
51106

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()}');
71114
});
72115

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+
});
86124

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+
});
89137

90-
if (!_isFirstFrameRendered) {
91-
onFirstFrameRendered?.call();
92-
_isFirstFrameRendered = true;
93-
}
138+
videoElement.onEnded.listen((dynamic _) {
139+
print('RTCVideoRenderer: videoElement.onEnded');
94140
});
141+
}
95142

96-
htmlView.onResize.listen((_) => _checkVideoSizeChanged(htmlView));
143+
MediaStream get srcObject => _srcObject;
97144

98-
_checkVideoSizeChanged(htmlView);
99-
}
145+
set srcObject(MediaStream stream) {
146+
if (videoElement == null) throw 'Call initialize before setting the stream';
100147

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;
108152
}
153+
_srcObject = stream;
154+
videoElement.srcObject = stream?.jsStream;
155+
videoElement.muted = stream?.ownerTag == 'local';
156+
value = value.copyWith(renderVideo: renderVideo);
109157
}
110158

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
123160
Future<void> dispose() async {
161+
super.dispose();
124162
await _srcObject?.dispose();
125-
126163
_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();
133166
}
134167
}
135168

136169
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),
142176
assert(mirror != null),
143177
super(key: key);
144178

145179
final RTCVideoRenderer _renderer;
146180
final RTCVideoViewObjectFit objectFit;
147-
final mirror;
148-
181+
final bool mirror;
149182
@override
150-
_RTCVideoViewState createState() => _RTCVideoViewState(_renderer);
183+
_RTCVideoViewState createState() => _RTCVideoViewState();
151184
}
152185

153186
class _RTCVideoViewState extends State<RTCVideoView> {
154-
_RTCVideoViewState(this._renderer);
155-
156-
final RTCVideoRenderer _renderer;
157-
double _aspectRatio;
187+
_RTCVideoViewState();
158188

159189
@override
160190
void initState() {
161191
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(() {}));
170193
}
171194

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}');
189203
}
190204

191205
@override
192206
Widget build(BuildContext context) {
193-
var renderVideo = _renderer._srcObject != null;
194207
return LayoutBuilder(
195208
builder: (BuildContext context, BoxConstraints constraints) {
196209
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+
));
198217
});
199218
}
200219
}

0 commit comments

Comments
 (0)
0