feat: Stats monitor for Track. (#290)
* feat: Stats monitor for Track. * fix analyze. * fix. * import sorter. * add monitor layer for example. * update. * update. * fix import sorter. * update. * import sorter. * fix typo for filename. * revert changes for VideoPublishOptions.videoCodec. * chore: improve code. * update.
This commit is contained in:
+32
-23
@@ -190,31 +190,40 @@ class _RoomPageState extends State<RoomPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: participantTracks.isNotEmpty
|
||||
? ParticipantWidget.widgetFor(participantTracks.first)
|
||||
: Container()),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participantTracks.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child:
|
||||
ParticipantWidget.widgetFor(participantTracks[index + 1]),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: participantTracks.isNotEmpty
|
||||
? ParticipantWidget.widgetFor(participantTracks.first,
|
||||
showStatsLayer: true)
|
||||
: Container()),
|
||||
if (widget.room.localParticipant != null)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: ControlsWidget(
|
||||
widget.room, widget.room.localParticipant!),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (widget.room.localParticipant != null)
|
||||
SafeArea(
|
||||
top: false,
|
||||
child:
|
||||
ControlsWidget(widget.room, widget.room.localParticipant!),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: SizedBox(
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participantTracks.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||
width: 180,
|
||||
height: 120,
|
||||
child: ParticipantWidget.widgetFor(
|
||||
participantTracks[index + 1]),
|
||||
),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,20 +7,24 @@ import 'package:livekit_example/theme.dart';
|
||||
|
||||
import 'no_video.dart';
|
||||
import 'participant_info.dart';
|
||||
import 'participant_stats.dart';
|
||||
|
||||
abstract class ParticipantWidget extends StatefulWidget {
|
||||
// Convenience method to return relevant widget for participant
|
||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack) {
|
||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
|
||||
{bool showStatsLayer = false}) {
|
||||
if (participantTrack.participant is LocalParticipant) {
|
||||
return LocalParticipantWidget(
|
||||
participantTrack.participant as LocalParticipant,
|
||||
participantTrack.videoTrack,
|
||||
participantTrack.isScreenShare);
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer);
|
||||
} else if (participantTrack.participant is RemoteParticipant) {
|
||||
return RemoteParticipantWidget(
|
||||
participantTrack.participant as RemoteParticipant,
|
||||
participantTrack.videoTrack,
|
||||
participantTrack.isScreenShare);
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer);
|
||||
}
|
||||
throw UnimplementedError('Unknown participant type');
|
||||
}
|
||||
@@ -29,6 +33,7 @@ abstract class ParticipantWidget extends StatefulWidget {
|
||||
abstract final Participant participant;
|
||||
abstract final VideoTrack? videoTrack;
|
||||
abstract final bool isScreenShare;
|
||||
abstract final bool showStatsLayer;
|
||||
final VideoQuality quality;
|
||||
|
||||
const ParticipantWidget({
|
||||
@@ -44,11 +49,14 @@ class LocalParticipantWidget extends ParticipantWidget {
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
|
||||
const LocalParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isScreenShare, {
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -63,11 +71,14 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
|
||||
const RemoteParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isScreenShare, {
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -136,7 +147,13 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
)
|
||||
: const NoVideoWidget(),
|
||||
),
|
||||
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
top: 30,
|
||||
right: 30,
|
||||
child: ParticipantStatsWidget(
|
||||
participant: widget.participant,
|
||||
)),
|
||||
// Bottom bar
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
|
||||
enum StatsType {
|
||||
kUnknown,
|
||||
kLocalAudioSender,
|
||||
kLocalVideoSender,
|
||||
kRemoteAudioReceiver,
|
||||
kRemoteVideoReceiver,
|
||||
}
|
||||
|
||||
class ParticipantStatsWidget extends StatefulWidget {
|
||||
const ParticipantStatsWidget({Key? key, required this.participant})
|
||||
: super(key: key);
|
||||
final Participant participant;
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ParticipantStatsWidgetState();
|
||||
}
|
||||
|
||||
class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
|
||||
List<EventsListener<TrackEvent>> listeners = [];
|
||||
StatsType statsType = StatsType.kUnknown;
|
||||
Map<String, String> stats = {};
|
||||
|
||||
void _setUpListener(Track track) {
|
||||
var listener = track.createListener();
|
||||
listeners.add(listener);
|
||||
if (track is LocalVideoTrack) {
|
||||
statsType = StatsType.kLocalVideoSender;
|
||||
listener.on<VideoSenderStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['video tx'] = 'total sent ${event.currentBitrate.toInt()} kpbs';
|
||||
event.stats.forEach((key, value) {
|
||||
stats['layer-$key'] =
|
||||
'${value.frameWidth ?? 0}x${value.frameHeight ?? 0} ${value.framesPerSecond?.toDouble() ?? 0} fps, ${event.bitrateForLayers[key] ?? 0} kbps';
|
||||
});
|
||||
var firstStats =
|
||||
event.stats['f'] ?? event.stats['h'] ?? event.stats['q'];
|
||||
if (firstStats != null) {
|
||||
stats['encoder'] = firstStats.encoderImplementation ?? '';
|
||||
stats['video codec'] =
|
||||
'${firstStats.mimeType}, ${firstStats.clockRate}hz, pt: ${firstStats.payloadType}';
|
||||
stats['qualityLimitationReason'] =
|
||||
firstStats.qualityLimitationReason ?? '';
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (track is RemoteVideoTrack) {
|
||||
statsType = StatsType.kRemoteVideoReceiver;
|
||||
listener.on<VideoReceiverStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['video rx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['video codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, pt: ${event.stats.payloadType}';
|
||||
stats['video size'] =
|
||||
'${event.stats.frameWidth}x${event.stats.frameHeight} ${event.stats.framesPerSecond?.toDouble()}fps';
|
||||
stats['video jitter'] = '${event.stats.jitter} s';
|
||||
stats['video decoder'] = '${event.stats.decoderImplementation}';
|
||||
//stats['video packets lost'] = '${event.stats.packetsLost}';
|
||||
//stats['video packets received'] = '${event.stats.packetsReceived}';
|
||||
stats['video frames received'] = '${event.stats.framesReceived}';
|
||||
stats['video frames decoded'] = '${event.stats.framesDecoded}';
|
||||
stats['video frames dropped'] = '${event.stats.framesDropped}';
|
||||
});
|
||||
});
|
||||
} else if (track is LocalAudioTrack) {
|
||||
statsType = StatsType.kLocalAudioSender;
|
||||
listener.on<AudioSenderStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['audio tx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['audio codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
||||
});
|
||||
});
|
||||
} else if (track is RemoteAudioTrack) {
|
||||
statsType = StatsType.kRemoteAudioReceiver;
|
||||
listener.on<AudioReceiverStatsEvent>((event) {
|
||||
setState(() {
|
||||
stats['audio rx'] = '${event.currentBitrate.toInt()} kpbs';
|
||||
stats['audio codec'] =
|
||||
'${event.stats.mimeType}, ${event.stats.clockRate}hz, ${event.stats.channels}ch, pt: ${event.stats.payloadType}';
|
||||
stats['audio jitter'] = '${event.stats.jitter} s';
|
||||
//stats['audio concealed samples'] =
|
||||
// '${event.stats.concealedSamples} / ${event.stats.concealmentEvents}';
|
||||
stats['audio packets lost'] = '${event.stats.packetsLost}';
|
||||
stats['audio packets received'] = '${event.stats.packetsReceived}';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onParticipantChanged() {
|
||||
for (var element in listeners) {
|
||||
element.dispose();
|
||||
}
|
||||
listeners.clear();
|
||||
for (var track in [
|
||||
...widget.participant.videoTracks,
|
||||
...widget.participant.audioTracks
|
||||
]) {
|
||||
if (track.track != null) {
|
||||
_setUpListener(track.track!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.participant.addListener(_onParticipantChanged);
|
||||
// trigger initial change
|
||||
_onParticipantChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
for (var element in listeners) {
|
||||
element.dispose();
|
||||
}
|
||||
widget.participant.removeListener(_onParticipantChanged);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
num sendBitrate = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Column(
|
||||
children:
|
||||
stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user