video rendering, local track creation
This commit is contained in:
@@ -16,3 +16,4 @@ export 'src/track/local_video_track.dart';
|
|||||||
export 'src/track/track_publication.dart';
|
export 'src/track/track_publication.dart';
|
||||||
export 'src/track/local_track_publication.dart';
|
export 'src/track/local_track_publication.dart';
|
||||||
export 'src/track/remote_track_publication.dart';
|
export 'src/track/remote_track_publication.dart';
|
||||||
|
export 'src/widget/video_track_renderer.dart';
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ class ConnectError extends LiveKitError {
|
|||||||
ConnectError([String msg = 'Failed to connect to server']) : super(msg);
|
ConnectError([String msg = 'Failed to connect to server']) : super(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TrackCreateError extends LiveKitError {
|
||||||
|
TrackCreateError([String msg = 'Failed to create track']) : super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
class TrackPublishError extends LiveKitError {
|
class TrackPublishError extends LiveKitError {
|
||||||
TrackPublishError([String msg = 'Failed to publish track']) : super(msg);
|
TrackPublishError([String msg = 'Failed to publish track']) : super(msg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class RemoteParticipant extends Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubscribedMediaTrack(MediaStreamTrack mediaTrack, String? sid) async {
|
addSubscribedMediaTrack(
|
||||||
|
MediaStreamTrack mediaTrack, MediaStream stream, String? sid) async {
|
||||||
if (sid == null) {
|
if (sid == null) {
|
||||||
var msg = 'addSubscribedMediaTrack received null sid';
|
var msg = 'addSubscribedMediaTrack received null sid';
|
||||||
delegate?.onTrackSubscriptionFailed(this, '', msg);
|
delegate?.onTrackSubscriptionFailed(this, '', msg);
|
||||||
@@ -50,7 +51,7 @@ class RemoteParticipant extends Participant {
|
|||||||
if (pub.kind == TrackType.AUDIO) {
|
if (pub.kind == TrackType.AUDIO) {
|
||||||
track = new Track(pub.kind, pub.name, mediaTrack);
|
track = new Track(pub.kind, pub.name, mediaTrack);
|
||||||
} else if (pub.kind == TrackType.VIDEO) {
|
} else if (pub.kind == TrackType.VIDEO) {
|
||||||
track = new VideoTrack(pub.name, mediaTrack);
|
track = new VideoTrack(pub.name, mediaTrack, stream);
|
||||||
} else {
|
} else {
|
||||||
var msg = 'unsupported track type ${pub.kind}';
|
var msg = 'unsupported track type ${pub.kind}';
|
||||||
delegate?.onTrackSubscriptionFailed(this, sid, msg);
|
delegate?.onTrackSubscriptionFailed(this, sid, msg);
|
||||||
|
|||||||
+5
-4
@@ -144,7 +144,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
|
|
||||||
RemoteParticipant _getOrCreateRemoteParticipant(
|
RemoteParticipant _getOrCreateRemoteParticipant(
|
||||||
String sid, ParticipantInfo? info) {
|
String sid, ParticipantInfo? info) {
|
||||||
var participant = participants[sid];
|
var participant = _participants[sid];
|
||||||
if (participant != null) {
|
if (participant != null) {
|
||||||
return participant;
|
return participant;
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
participant = RemoteParticipant.fromInfo(_engine.client, info);
|
participant = RemoteParticipant.fromInfo(_engine.client, info);
|
||||||
}
|
}
|
||||||
participant.roomDelegate = this;
|
participant.roomDelegate = this;
|
||||||
participants[sid] = participant;
|
_participants[sid] = participant;
|
||||||
|
|
||||||
return participant;
|
return participant;
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var participant = _getOrCreateRemoteParticipant(parsed.item1, null);
|
var participant = _getOrCreateRemoteParticipant(parsed.item1, null);
|
||||||
participant.addSubscribedMediaTrack(track, trackSid);
|
participant.addSubscribedMediaTrack(track, stream, trackSid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleParticipantDisconnect(String sid) {
|
_handleParticipantDisconnect(String sid) {
|
||||||
@@ -292,7 +292,8 @@ class Room extends ChangeNotifier with ParticipantDelegate {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var track in participant.tracks.values) {
|
var toRemove = List.from(participant.tracks.values);
|
||||||
|
for (var track in toRemove) {
|
||||||
participant.unpublishTrack(track.sid, true);
|
participant.unpublishTrack(track.sid, true);
|
||||||
}
|
}
|
||||||
delegate?.onParticipantDisconnected(participant);
|
delegate?.onParticipantDisconnected(participant);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
import '../errors.dart';
|
||||||
import '../proto/livekit_models.pb.dart';
|
import '../proto/livekit_models.pb.dart';
|
||||||
|
import 'options.dart';
|
||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
|
||||||
class LocalAudioTrack extends Track {
|
class LocalAudioTrack extends Track {
|
||||||
LocalAudioTrack(String name, MediaStreamTrack track) : super(TrackType.AUDIO, name, track);
|
MediaStream mediaStream;
|
||||||
|
|
||||||
|
LocalAudioTrack(String name, MediaStreamTrack track, this.mediaStream)
|
||||||
|
: super(TrackType.AUDIO, name, track);
|
||||||
|
|
||||||
|
Future<LocalAudioTrack> createTrack(LocalAudioTrackOptions? options) async {
|
||||||
|
try {
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
"audio": true,
|
||||||
|
"video": false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stream.getAudioTracks().length == 0) {
|
||||||
|
return Future.error(TrackCreateError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalAudioTrack("", stream.getVideoTracks().first, stream);
|
||||||
|
} catch (e) {
|
||||||
|
return Future.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
|
||||||
|
import '../errors.dart';
|
||||||
|
import 'options.dart';
|
||||||
import 'video_track.dart';
|
import 'video_track.dart';
|
||||||
|
|
||||||
class LocalVideoTrack extends VideoTrack {
|
class LocalVideoTrack extends VideoTrack {
|
||||||
LocalVideoTrack(String name, MediaStreamTrack mediaTrack)
|
LocalVideoTrack(String name, MediaStreamTrack mediaTrack, MediaStream stream)
|
||||||
: super(name, mediaTrack);
|
: super(name, mediaTrack, stream);
|
||||||
|
|
||||||
|
Future<LocalVideoTrack> createCameraTrack(
|
||||||
|
LocalVideoTrackOptions? options) async {
|
||||||
|
if (options == null) {
|
||||||
|
options = LocalVideoTrackOptions(params: VideoPresets.qhd);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
"audio": false,
|
||||||
|
"video": options.mediaConstraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stream.getVideoTracks().length == 0) {
|
||||||
|
return Future.error(TrackCreateError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocalVideoTrack("", stream.getVideoTracks().first, stream);
|
||||||
|
} catch (e) {
|
||||||
|
return Future.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
class LocalVideoTrackOptions {
|
||||||
|
CameraPosition position = CameraPosition.FRONT;
|
||||||
|
VideoParameter params;
|
||||||
|
|
||||||
|
LocalVideoTrackOptions({
|
||||||
|
required VideoParameter params,
|
||||||
|
CameraPosition? position,
|
||||||
|
}) : this.params = params {
|
||||||
|
if (position != null) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> get mediaConstraints {
|
||||||
|
return {
|
||||||
|
"mandatory": params.mediaConstraints,
|
||||||
|
"facingMode": position == CameraPosition.FRONT ? "user" : "environment",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CameraPosition {
|
||||||
|
FRONT,
|
||||||
|
BACK,
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoParameter {
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int fps;
|
||||||
|
int? bitrate;
|
||||||
|
|
||||||
|
VideoParameter(
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fps, {
|
||||||
|
int? bitrate,
|
||||||
|
}) : this.bitrate = bitrate;
|
||||||
|
|
||||||
|
Map<String, dynamic> get mediaConstraints {
|
||||||
|
return {
|
||||||
|
"minWidth": this.width,
|
||||||
|
"minHeight": this.height,
|
||||||
|
"minFrameRate": this.fps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoPresets {
|
||||||
|
static final qvga = VideoParameter(320, 180, 15);
|
||||||
|
static final vga = VideoParameter(640, 360, 30);
|
||||||
|
static final qhd = VideoParameter(960, 540, 30);
|
||||||
|
static final hd = VideoParameter(1280, 720, 30);
|
||||||
|
static final fhd = VideoParameter(1920, 1080, 30);
|
||||||
|
|
||||||
|
static final List<VideoParameter> all = [
|
||||||
|
qvga,
|
||||||
|
vga,
|
||||||
|
qhd,
|
||||||
|
hd,
|
||||||
|
fhd,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalAudioTrackOptions {}
|
||||||
@@ -11,6 +11,8 @@ class TrackDimension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Track {
|
class Track {
|
||||||
|
static const ScreenShareName = "screen";
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
TrackType kind;
|
TrackType kind;
|
||||||
MediaStreamTrack mediaTrack;
|
MediaStreamTrack mediaTrack;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class TrackPublication {
|
|||||||
updateFromInfo(info);
|
updateFromInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isScreenShare =>
|
||||||
|
kind == TrackType.VIDEO && name == Track.ScreenShareName;
|
||||||
|
|
||||||
updateFromInfo(TrackInfo info) {
|
updateFromInfo(TrackInfo info) {
|
||||||
muted = info.muted;
|
muted = info.muted;
|
||||||
simulcasted = info.simulcast;
|
simulcasted = info.simulcast;
|
||||||
|
|||||||
@@ -4,14 +4,8 @@ import '../proto/livekit_models.pb.dart';
|
|||||||
import 'track.dart';
|
import 'track.dart';
|
||||||
|
|
||||||
class VideoTrack extends Track {
|
class VideoTrack extends Track {
|
||||||
VideoTrack(String name, MediaStreamTrack mediaTrack)
|
MediaStream mediaStream;
|
||||||
|
|
||||||
|
VideoTrack(String name, MediaStreamTrack mediaTrack, this.mediaStream)
|
||||||
: super(TrackType.VIDEO, name, mediaTrack);
|
: super(TrackType.VIDEO, name, mediaTrack);
|
||||||
|
|
||||||
// TODO: keep list of video renderers
|
|
||||||
|
|
||||||
@override
|
|
||||||
stop() {
|
|
||||||
super.stop();
|
|
||||||
// TODO: remove renderer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import '../track/video_track.dart';
|
||||||
|
import '../track/local_video_track.dart';
|
||||||
|
|
||||||
|
class VideoTrackRenderer extends StatefulWidget {
|
||||||
|
final VideoTrack track;
|
||||||
|
final RTCVideoRenderer renderer;
|
||||||
|
|
||||||
|
VideoTrackRenderer(this.track)
|
||||||
|
: renderer = RTCVideoRenderer(),
|
||||||
|
super(key: ValueKey(track.sid));
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _VideoTrackRendererState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoTrackRendererState extends State<VideoTrackRenderer> {
|
||||||
|
final _renderer = RTCVideoRenderer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_renderer.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initRenderer() async {
|
||||||
|
await _renderer.initialize();
|
||||||
|
_renderer.srcObject = widget.track.mediaStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var isLocal = widget.track is LocalVideoTrack;
|
||||||
|
return RTCVideoView(
|
||||||
|
_renderer,
|
||||||
|
mirror: isLocal,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user