8000 Refactor RTCVideoView for web and solve the problem that resize canno… · next-coder/flutter-webrtc@66c31e0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 66c31e0

Browse files
committed
Refactor RTCVideoView for web and solve the problem that resize cannot be refreshed.
1 parent 7b6e110 commit 66c31e0

File tree

1 file changed

+171
-160
lines changed

1 file changed

+171
-160
lines changed

lib/src/web/rtc_video_view.dart

Lines changed: 171 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -2,205 +2,216 @@ 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 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+
}
2655

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

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

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

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

38-
int get rotation => 0;
86+
MediaStream _srcObject;
3987

40-
double get width => _width ?? 1080;
88+
bool get muted => videoElement?.muted ?? true;
4189

42-
double get height => _height ?? 1920;
90+
set muted(bool mute) => videoElement?.muted = mute;
4391

44-
int get textureId => 0;
92+
bool get renderVideo => srcObject != null;
4593

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

49-
MediaStream get srcObject => _srcObject;
102+
// Allows Safari iOS to play the video inline
103+
videoElement.setAttribute('playsinline', 'true');
50104

51-
set srcObject(MediaStream stream) {
52-
_srcObject = stream;
105+
// ignore: undefined_prefixed_name
106+
ui.platformViewRegistry.registerViewFactory(
107+
'RTCVideoRenderer-$textureId', (int viewId) => videoElement);
53108

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

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

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');
72136
});
73-
htmlElementView = HtmlElementView(viewType: stream.id);
74-
if (onStateChanged != null) {
75-
onStateChanged();
76-
}
77137
}
78138

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;
122146
}
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);
123163
}
124164

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');
134172
}
135173

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();
139181
}
140182
}
141183

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),
148191
assert(mirror != null),
149192
super(key: key);
150193

151194
final RTCVideoRenderer _renderer;
152195
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;
171197

172198
@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));
184203
}
185204

186205
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+
));
205216
}
206217
}

0 commit comments

Comments
 (0)
0