publishVideoTrack method

Future<LocalTrackPublication<LocalVideoTrack>> publishVideoTrack(
  1. LocalVideoTrack track, {
  2. VideoPublishOptions? publishOptions,
})

Publish a LocalVideoTrack to the Room. For most cases, using setCameraEnabled would be simpler and recommended.

Implementation

Future<LocalTrackPublication<LocalVideoTrack>> publishVideoTrack(
  LocalVideoTrack track, {
  VideoPublishOptions? publishOptions,
}) async {
  if (videoTrackPublications.any((e) => e.track?.mediaStreamTrack.id == track.mediaStreamTrack.id)) {
    throw TrackPublishException('track already exists');
  }

  // Use defaultPublishOptions if options is null
  publishOptions ??= track.lastPublishOptions ?? room.roomOptions.defaultVideoPublishOptions;

  if (publishOptions.videoCodec.toLowerCase() != publishOptions.videoCodec) {
    publishOptions = publishOptions.copyWith(
      videoCodec: publishOptions.videoCodec.toLowerCase(),
    );
  }

  if (room.engine.enabledPublishCodecs?.isNotEmpty ?? false) {
    // fallback to a supported codec if it is not supported
    if (!room.engine.enabledPublishCodecs!
        .where((c) => c.mime.startsWith('video/'))
        .where((c) => videoCodecs.any((v) => c.mime.toLowerCase().endsWith(v)))
        .any((c) => publishOptions?.videoCodec == mimeTypeToVideoCodecString(c.mime))) {
      publishOptions = publishOptions.copyWith(
        videoCodec: mimeTypeToVideoCodecString(room.engine.enabledPublishCodecs![0].mime).toLowerCase(),
      );
    }
  }

  // handle SVC publishing
  final isSVC = isSVCCodec(publishOptions.videoCodec);
  if (isSVC) {
    if (!room.roomOptions.dynacast) {
      room.engine.roomOptions = room.roomOptions.copyWith(dynacast: true);
    }

    if (publishOptions.scalabilityMode == null) {
      publishOptions = publishOptions.copyWith(
        scalabilityMode: 'L3T3_KEY',
      );
    }

    // vp9 svc with screenshare has problem to encode, always use L1T3 here
    if (track.source == TrackSource.screenShareVideo) {
      publishOptions = publishOptions.copyWith(
        scalabilityMode: 'L1T3',
      );
    }
  }

  // use finalraints passed to getUserMedia by default
  VideoDimensions dimensions = track.currentOptions.params.dimensions;

  if (kIsWeb || lkPlatformIsMobile()) {
    // getSettings() is only implemented for Web & Mobile
    try {
      // try to use getSettings for more accurate resolution
      final settings = track.mediaStreamTrack.getSettings();
      if ((settings['width'] is int && settings['width'] as int > 0) &&
          (settings['height'] is int && settings['height'] as int > 0)) {
        dimensions = dimensions.copyWith(width: settings['width'] as int);
        dimensions = dimensions.copyWith(height: settings['height'] as int);
      }
    } catch (_) {
      logger.warning('Failed to call `mediaStreamTrack.getSettings()`');
    }
  }

  logger.fine('Compute encodings with resolution: ${dimensions}, options: ${publishOptions}');

  // Video encodings and simulcasts
  var encodings = Utils.computeVideoEncodings(
    isScreenShare: track.source == TrackSource.screenShareVideo,
    dimensions: dimensions,
    options: publishOptions,
    codec: publishOptions.videoCodec,
  );

  logger.fine('Using encodings: ${encodings?.map((e) => e.toMap())}');

  final simulcastCodecs = <lk_rtc.SimulcastCodec>[
    lk_rtc.SimulcastCodec(
      codec: publishOptions.videoCodec,
      cid: track.getCid(),
    ),
  ];

  if (publishOptions.backupVideoCodec.enabled && publishOptions.backupVideoCodec.codec != publishOptions.videoCodec) {
    simulcastCodecs.add(lk_rtc.SimulcastCodec(
      codec: publishOptions.backupVideoCodec.codec.toLowerCase(),
      cid: '',
    ));
  }

  final layers = Utils.computeVideoLayers(
    dimensions,
    encodings,
    isSVC,
  );

  if (room.engine.isClosed) {
    throw UnexpectedConnectionState('cannot publish track when not connected');
  }

  logger.fine('Video layers: ${layers.map((e) => e)}');

  Future<lk_models.TrackInfo> negotiate() async {
    track.transceiver = await room.engine.createTransceiverRTCRtpSender(track, publishOptions!, encodings);

    if (lkBrowser() != BrowserType.firefox) {
      await room.engine.setPreferredCodec(
        track.transceiver!,
        'video',
        publishOptions.videoCodec,
      );
      track.codec = publishOptions.videoCodec;
    }

    if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
      final degradationPreference = publishOptions.degradationPreference ??
          getDefaultDegradationPreference(
            track,
          );
      await track.setDegradationPreference(degradationPreference);
    }

    if (kIsWeb && lkBrowser() == BrowserType.firefox && track.kind == TrackType.AUDIO) {
      //TOOD:
    } else if (isSVCCodec(publishOptions.videoCodec) && encodings?.first.maxBitrate != null) {
      room.engine.publisher?.setTrackBitrateInfo(TrackBitrateInfo(
          cid: track.getCid(),
          transceiver: track.transceiver,
          codec: publishOptions.videoCodec,
          maxbr: encodings![0].maxBitrate! ~/ 1000));
    }

    await room.engine.negotiate();

    return lk_models.TrackInfo();
  }

  final req = lk_rtc.AddTrackRequest(
    cid: track.getCid(),
    name: publishOptions.name ??
        (track.source == TrackSource.screenShareVideo
            ? VideoPublishOptions.defaultScreenShareName
            : VideoPublishOptions.defaultCameraName),
    type: track.kind.toPBType(),
    source: track.source.toPBType(),
    encryption: room.roomOptions.lkEncryptionType,
    simulcastCodecs: simulcastCodecs,
    muted: track.muted,
    stream: buildStreamId(publishOptions, track.source),
  );

  // video specific
  if (dimensions.width > 0 && dimensions.height > 0) {
    req.width = dimensions.width;
    req.height = dimensions.height;
  }

  if (layers.isNotEmpty) {
    req.layers
      ..clear()
      ..addAll(layers);
  }
  late lk_models.TrackInfo trackInfo;
  if (room.engine.enabledPublishCodecs?.isNotEmpty ?? false) {
    final rets = await Future.wait<lk_models.TrackInfo>([room.engine.addTrack(req), negotiate()]);
    trackInfo = rets[0];
  } else {
    trackInfo = await room.engine.addTrack(req);

    String? primaryCodecMime;
    for (var codec in trackInfo.codecs) {
      primaryCodecMime ??= codec.mimeType;
    }

    if (primaryCodecMime != null) {
      final updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
      if (updatedCodec != publishOptions.videoCodec) {
        logger.fine(
          'requested a different codec than specified by serverRequested: ${publishOptions.videoCodec}, server: ${updatedCodec}',
        );
        publishOptions = publishOptions.copyWith(
          videoCodec: updatedCodec,
        );
        // recompute encodings since bitrates/etc could have changed
        encodings = Utils.computeVideoEncodings(
          isScreenShare: track.source == TrackSource.screenShareVideo,
          dimensions: dimensions,
          options: publishOptions,
          codec: publishOptions.videoCodec,
        );
      }
    }

    final transceiverInit = rtc.RTCRtpTransceiverInit(
      direction: rtc.TransceiverDirection.SendOnly,
      sendEncodings: encodings,
    );

    logger.fine('publishVideoTrack publisher: ${room.engine.publisher}');

    track.transceiver = await room.engine.publisher?.pc.addTransceiver(
      track: track.mediaStreamTrack,
      kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo,
      init: transceiverInit,
    );

    if (lkBrowser() != BrowserType.firefox) {
      await room.engine.setPreferredCodec(
        track.transceiver!,
        'video',
        publishOptions.videoCodec,
      );
      track.codec = publishOptions.videoCodec;
    }

    if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
      final degradationPreference = publishOptions.degradationPreference ??
          getDefaultDegradationPreference(
            track,
          );
      await track.setDegradationPreference(degradationPreference);
    }

    if (kIsWeb && lkBrowser() == BrowserType.firefox && track.kind == TrackType.AUDIO) {
      //TOOD:
    } else if (isSVCCodec(publishOptions.videoCodec) && encodings?.first.maxBitrate != null) {
      room.engine.publisher?.setTrackBitrateInfo(TrackBitrateInfo(
          cid: track.getCid(),
          transceiver: track.transceiver,
          codec: publishOptions.videoCodec,
          maxbr: encodings![0].maxBitrate! ~/ 1000));
    }

    await room.engine.negotiate();
  }

  logger.fine('publishVideoTrack engine.addTrack response: ${trackInfo}');

  track.lastPublishOptions = publishOptions;

  await track.start();

  final pub = LocalTrackPublication<LocalVideoTrack>(
    participant: this,
    info: trackInfo,
    track: track,
  );
  addTrackPublication(pub);
  pub.backupVideoCodec = publishOptions.backupVideoCodec;

  // did publish
  await track.onPublish();
  await track.processor?.onPublish(room);

  final listener = track.createListener();
  listener.on((TrackEndedEvent event) async {
    logger.fine('TrackEndedEvent: ${event.track}');
    await removePublishedTrack(pub.sid);
  });

  [events, room.events].emit(LocalTrackPublishedEvent(
    participant: this,
    publication: pub,
  ));

  return pub;
}