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:

  1. 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(),
        ),
      ),
    )
    
  2. 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),
            ),
          ),
        ),
      ),
    )
    
  3. 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
      ),
    )
    
  4. 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

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