CardVideoDisplay class
A stateful widget rendering WebRTC video streams with advanced lifecycle management.
Handles RTCVideoRenderer initialization, MediaStream attachment, video track polling, and layered content rendering (placeholder, video, overlay). Automatically detects video track availability and polls for tracks when stream exists but has no tracks.
Lifecycle & State Management:
- Creates RTCVideoRenderer in
initState()
and initializes asynchronously - Attaches stream in
_attachStream()
after renderer initialization - Monitors stream changes in
didUpdateWidget()
(detects stream replacement or track availability changes) - Polls for video tracks when stream exists but getVideoTracks() is empty
- Disposes renderer and cancels polling timer in
dispose()
- Tracks
_streamReady
boolean (true when stream has video tracks and is attached)
Stream Attachment Flow:
1. _attachStream(stream) called
2. Cancel any active polling timer
3. Check stream status:
- If stream == null:
- If !maintainRendererOnNullStream: renderer.srcObject = null
- Set streamReady = false
- If stream has video tracks:
- renderer.srcObject = stream
- Set streamReady = true
- If stream has no video tracks:
- Set streamReady = false
- Start polling timer to check again later
4. setState() if streamReady changed
Video Track Polling:
- Triggered when stream exists but getVideoTracks().isEmpty
- Polls at
streamPollInterval
(default 120ms, min 60ms) - Continues until tracks detected or stream changes
- Useful for streams where tracks are added asynchronously
- Automatically cancelled when stream changes or widget disposes
Widget Update Detection:
// Stream replacement:
if (newStream != oldStream) { _attachStream(newStream); }
// Track availability change (e.g., track added/removed):
if (oldStream.hasVideoTracks != newStream.hasVideoTracks) {
_attachStream(newStream);
}
Rendering Pipeline:
1. _buildVideoSurface():
- Create RTCVideoView with mirror/objectFit settings
- Call videoBuilder hook (if provided)
2. _buildLayeredContent():
- Positioned.fill → videoSurface (always present)
- Positioned.fill → placeholder (if !streamReady and placeholder!=null)
- Positioned.fill → overlay (if overlay!=null)
- Wrap in Stack(fit: StackFit.expand)
3. _buildContainer():
- Wrap layeredContent in Container with:
- margin, padding, alignment, constraints
- decoration (merged with backgroundColor)
- clipBehavior (auto-detected from decoration)
- Call containerBuilder hook (if provided)
Clip Behavior Auto-Detection:
if (clipBehavior provided) {
use clipBehavior;
} else if (decoration has borderRadius or non-rectangle shape) {
use Clip.antiAlias; // smooth rounded corners
} else {
use Clip.none; // no clipping overhead
}
Common Use Cases:
-
Remote Participant Video:
CardVideoDisplay( options: CardVideoDisplayOptions( remoteProducerId: participant.videoId, eventType: EventType.conference, forceFullDisplay: false, // contain mode videoStream: participant.stream, backgroundColor: Colors.black, placeholder: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ MiniCard(options: MiniCardOptions( initials: participant.name[0], fontSize: 32, )), SizedBox(height: 8), Text( participant.name, style: TextStyle(color: Colors.white), ), ], ), ), overlay: Positioned( bottom: 8, left: 8, child: participant.muted ? Icon(Icons.mic_off, color: Colors.red, size: 20) : SizedBox.shrink(), ), ), )
-
Local Camera Preview (Mirrored):
CardVideoDisplay( options: CardVideoDisplayOptions( remoteProducerId: 'local_camera', eventType: EventType.conference, forceFullDisplay: true, // cover mode videoStream: localStream, backgroundColor: Colors.grey[900]!, doMirror: true, // horizontal flip for natural preview decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue, width: 2), ), constraints: BoxConstraints.tightFor(width: 200, height: 150), overlay: Positioned( top: 8, right: 8, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(4), ), child: Text( 'You', style: TextStyle(color: Colors.white, fontSize: 12), ), ), ), ), )
-
Screenshare Display:
CardVideoDisplay( options: CardVideoDisplayOptions( remoteProducerId: presenter.screenId, eventType: EventType.conference, forceFullDisplay: false, // contain to preserve aspect ratio videoStream: screenshareStream, backgroundColor: Colors.black, placeholder: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.screen_share, size: 80, color: Colors.white38), SizedBox(height: 16), Text( 'Waiting for screenshare...', style: TextStyle(color: Colors.white54, fontSize: 16), ), ], ), ), maintainRendererOnNullStream: true, // keep last frame on disconnect streamPollInterval: Duration(milliseconds: 100), // check frequently ), )
-
Gallery Grid Tile:
CardVideoDisplay( options: CardVideoDisplayOptions( remoteProducerId: participant.videoId, eventType: EventType.conference, forceFullDisplay: true, // cover for uniform grid videoStream: participant.stream, backgroundColor: Colors.grey[850]!, decoration: BoxDecoration( border: participant.isSpeaking ? Border.all(color: Colors.green, width: 3) : null, ), placeholder: Center( child: MiniCard( options: MiniCardOptions( initials: participant.name.substring(0, 2).toUpperCase(), fontSize: 24, customStyle: BoxDecoration( color: Colors.blueGrey, shape: BoxShape.circle, ), ), ), ), overlay: Stack( children: [ // Name badge at bottom Positioned( bottom: 8, left: 8, right: 8, child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(4), ), child: Text( participant.name, style: TextStyle(color: Colors.white, fontSize: 12), overflow: TextOverflow.ellipsis, ), ), ), // Mute indicator at top-right if (participant.muted) Positioned( top: 8, right: 8, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: Icon(Icons.mic_off, color: Colors.white, size: 16), ), ), ], ), ), )
Override Integration:
Integrates with MediasfuUICustomOverrides
for global styling:
overrides: MediasfuUICustomOverrides(
cardVideoDisplayOptions: ComponentOverride<CardVideoDisplayOptions>(
builder: (existingOptions) => CardVideoDisplayOptions(
remoteProducerId: existingOptions.remoteProducerId,
eventType: existingOptions.eventType,
forceFullDisplay: existingOptions.forceFullDisplay,
videoStream: existingOptions.videoStream,
backgroundColor: Colors.grey[900]!,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white24),
),
placeholder: Center(
child: CircularProgressIndicator(color: Colors.blue),
),
),
),
),
Performance Notes:
- Stateful widget (maintains _renderer, _pollTimer, _streamReady)
- RTCVideoRenderer initialized once per lifecycle
- Stream attachment only when stream/tracks change (not every frame)
- Polling timer cancelled immediately on stream change (no redundant timers)
- setState() called only when _streamReady changes (efficient rebuilds)
- Builder hooks called during every build (not cached)
Implementation Details:
- Uses flutter_webrtc RTCVideoRenderer for native video rendering
- Checks mounted before setState() to prevent errors after dispose
- Stream polling uses single-shot Timer (not periodic) to avoid overlap
- Track availability checked via getVideoTracks().isNotEmpty
- Decoration merges with backgroundColor (decoration.color takes precedence)
- Placeholder uses IgnorePointer to pass touches through to video
- Overlay receives all pointer events (not IgnorePointer)
Typical Usage Context:
- Video conferencing participant tiles
- Local camera preview
- Screenshare display
- Picture-in-picture mini player
- Gallery grid video cells
- Inheritance
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- CardVideoDisplay
Constructors
- CardVideoDisplay({Key? key, required CardVideoDisplayOptions options})
-
const
Properties
- hashCode → int
-
The hash code for this object.
no setterinherited
- key → Key?
-
Controls how one widget replaces another widget in the tree.
finalinherited
- options → CardVideoDisplayOptions
-
final
- runtimeType → Type
-
A representation of the runtime type of the object.
no setterinherited
Methods
-
createElement(
) → StatefulElement -
Creates a StatefulElement to manage this widget's location in the tree.
inherited
-
createState(
) → _CardVideoDisplayState -
Creates the mutable state for this widget at a given location in the tree.
override
-
debugDescribeChildren(
) → List< DiagnosticsNode> -
Returns a list of DiagnosticsNode objects describing this node's
children.
inherited
-
debugFillProperties(
DiagnosticPropertiesBuilder properties) → void -
Add additional properties associated with the node.
inherited
-
noSuchMethod(
Invocation invocation) → dynamic -
Invoked when a nonexistent method or property is accessed.
inherited
-
toDiagnosticsNode(
{String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode -
Returns a debug representation of the object that is used by debugging
tools and by DiagnosticsNode.toStringDeep.
inherited
-
toString(
{DiagnosticLevel minLevel = DiagnosticLevel.info}) → String -
A string representation of this object.
inherited
-
toStringDeep(
{String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) → String -
Returns a string representation of this node and its descendants.
inherited
-
toStringShallow(
{String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String -
Returns a one-line detailed description of the object.
inherited
-
toStringShort(
) → String -
A short, textual description of this widget.
inherited
Operators
-
operator ==(
Object other) → bool -
The equality operator.
inherited