File

src/lib/components/video-box/video-box.component.ts

Description

Component for rendering an audio/video stream received from a remote publisher

In addition to rendering the video content, this will keep track of the streaming performance and request higher/lower bitrate streams when simulcast is available.

Implements

OnInit OnChanges OnDestroy AfterViewInit

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector janus-video-box
styleUrls ./video-box.component.scss,
../../styles/video-styles.scss
templateUrl ./video-box.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(janusService: JanusService)
Parameters :
Name Type Optional
janusService JanusService No

Inputs

devices
Type : Devices

Requested output device (speaker). If available, this will dynamically change the speaker device. This is not available in chrome on android

mode
Type : "speaker" | "grid"

Current mode of the videoroom

remoteFeed
Type : RemoteFeed

RemoteFeed object

Outputs

maximize
Type : EventEmitter

Event for switching to speaker/grid view

requestSubstream
Type : EventEmitter

Event for switching to speaker/grid view

Methods

monitorVideoQuality
monitorVideoQuality(slowLink: boolean)

Called anytime the remoteFeed changes plus on a set interval

Parameters :
Name Type Optional
slowLink boolean No
Returns : void
onDeviceChange
onDeviceChange(devices: Devices)

Attempts to change speaker if requested

Parameters :
Name Type Optional
devices Devices No
Returns : void
onMaximize
onMaximize()

Callback for the maximize button

Returns : void
setupSubscriptions
setupSubscriptions()

Interval for checking video quality

Returns : void
switchSubstream
switchSubstream(substreamId: number)

Called to request a new substream

Parameters :
Name Type Optional
substreamId number No
Returns : void

Properties

videoQualityHelper
Type : VideoQualityHelper

Helper class for monitoring video quality and determining when to request a new substream

Accessors

devices
setdevices(devices)
Parameters :
Name Optional
devices No
Returns : void
import * as moment from 'moment';

import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  OnChanges,
  Output,
  ViewChild,
} from '@angular/core';

import { Subject, interval, fromEvent } from 'rxjs';
import { first, takeUntil, debounce } from 'rxjs/operators';

import { RemoteFeed, JanusRole, Devices, RequestSubstreamEvent } from '../../models';
import { randomString } from '../../shared';
import { JanusService } from '../../services/janus.service';

import { VideoQualityHelper } from './video-quality-helper';


/**
 * Component for rendering an audio/video stream received from a remote publisher
 *
 * In addition to rendering the video content, this will keep track of the streaming
 * performance and request higher/lower bitrate streams when simulcast is available.
 */
@Component({
  selector: 'janus-video-box',
  templateUrl: './video-box.component.html',
  styleUrls: [
    './video-box.component.scss',
    '../../styles/video-styles.scss',
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoBoxComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {

  /** RemoteFeed object */
  @Input() remoteFeed: RemoteFeed;

  /** Current mode of the videoroom */
  @Input() mode: 'speaker' | 'grid';

  /** Requested output device (speaker). If available, this will dynamically change the
   * speaker device. This is not available in chrome on android
   */
  @Input()
  get devices(): Devices {
    return this.localDevices;
  }
  set devices(devices: Devices) {
    this.localDevices = devices;
    this.onDeviceChange(devices);
  }

  /** Event for switching to speaker/grid view */
  @Output()
  maximize = new EventEmitter<RemoteFeed>();

  /** Event for switching to speaker/grid view */
  @Output()
  requestSubstream = new EventEmitter<RequestSubstreamEvent>();

  /** @internal */
  public videoId: string;

  /** @internal */
  public optionsOpen = false;

  /** @internal */
  public videoAvailable = false;

  /** Helper class for monitoring video quality and determining when to request a new substream */
  videoQualityHelper: VideoQualityHelper; // public for testing purposes

  /** @internal */
  private localDevices: Devices;

  /** @internal */
  private destroy$ = new Subject();

  /** @internal */
  @ViewChild('videoElement') video: ElementRef;

  constructor(
    private janusService: JanusService
  ) {
    this.videoQualityHelper = new VideoQualityHelper(3);
  }

  ngOnInit(): void {
    // Set my unique id for the video
    this.videoId = 'video-' + this.remoteFeed.id + this.mode;
    this.setupSubscriptions();
  }

  ngAfterViewInit(): void {
    this._attachMediaStream();
    this.setSpeaker(this.devices);
  }

  ngOnChanges(changes): void {
    if ('remoteFeed' in changes) {
      // If there's a change in the remoteFeed, run the video quality monitor task
      let slowLink = false;

      if (
        changes.remoteFeed.previousValue
        && changes.remoteFeed.previousValue.slowLink !== changes.remoteFeed.currentValue.slowLink
      ) {
        slowLink = true;
      }

      this.monitorVideoQuality(slowLink);
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    if (this.video) {
      this.video.nativeElement.pause();
    }
  }

  /** Interval for checking video quality */
  setupSubscriptions(): void {
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => {
      this.monitorVideoQuality(false);
    });
  }

  /** @internal */
  _attachMediaStream(): void {
    this.janusService.attachMediaStream(this.videoId, this.remoteFeed.streamId);
  }

  /** @internal */
  private setSpeaker(devices: Devices): void {
    // Given the devices, set the output sound device
    if (
      this.video
      && this.video.nativeElement
      && this.video.nativeElement.setSinkId
      && devices
      && devices.speakerDeviceId
    ) {
      this.video.nativeElement.setSinkId(devices.speakerDeviceId);
    }
  }

  /** @internal */
  onPlay(): void {
    this.videoAvailable = true;
  }

  /** Called anytime the `remoteFeed` changes plus on a set interval */
  monitorVideoQuality(slowLink: boolean): void {
    // Periodic task to monitor the video quality and change substream if necessary

    if (!this.remoteFeed) {
      // If we don't have a remoteFeed, nothing we can do here
      return;
    }

    if (!this.videoAvailable && this.video) {
      // Sometimes this needs a kick start. For example, if the user takes a second to click
      // the "allow" button for video/mic access, the autoplay on the video element won't
      // actually autoplay
      this.video.nativeElement.play();
    }

    const currentSubstream = this.remoteFeed.currentSubstream;
    if (this.remoteFeed.numVideoTracks === 0 || slowLink) {
      this.videoQualityHelper.streamError(currentSubstream);
      if (currentSubstream > 0) {
        this.switchSubstream(currentSubstream - 1);
      }
    } else {
      const newSubstream = this.videoQualityHelper.ping(currentSubstream);
      if (newSubstream > currentSubstream) {
        this.videoQualityHelper.streamEnd(currentSubstream);
        this.switchSubstream(newSubstream);
      }
    }
  }

  /** Called to request a new substream */
  switchSubstream(substreamId: number): void {
    // Switch the substream if we haven't already requested this substream
    if (this.remoteFeed.requestedSubstream !== substreamId) {
      console.log('switching substream', substreamId, this.videoId);
      this.requestSubstream.emit({feed: this.remoteFeed, substreamId});
    }
  }

  /** Callback for the maximize button */
  onMaximize(): void {
    this.maximize.emit(this.remoteFeed);
  }

  /** Attempts to change speaker if requested */
  onDeviceChange(devices: Devices): void {
    this.setSpeaker(devices);
  }
}
<div class='video-container'>
  <div class='interior-box'>
    <video
      #videoElement
      id='{{videoId}}'
      autoplay
      playsinline
      (play)='onPlay()'
    ></video>

    <div
      *ngIf="remoteFeed.displayName"
      data-cy='video-box-display-name'
      class='overlay display-name'>
      {{ remoteFeed.displayName }}
    </div>

    <div
      data-cy='video-box-maximize-button'
      class='overlay maximize'
      (click)='onMaximize()' 
      >

      <i
        *ngIf='mode === "grid"'
        class="fas fa-expand"
        matTooltip="Show Full Size">
      </i>

      <i
        *ngIf='mode === "speaker"'
        class="fas fa-compress"
        matTooltip="Show All Speakers">
      </i>
    </div>

    <div 
      *ngIf='!videoAvailable'
      class='loading-blocker'>
      <p> Loading... </p>
    </div>
  </div>
</div>

./video-box.component.scss

div.display-name {
    display: flex;
    z-index: 1;

    span.separator {
        margin: 0 5px 0 10px;
    }

    i.fas {
        font-size: 14px;
        margin: 0 5px 0 5px;
        cursor: pointer;
    }
}

div.maximize {
   left: auto !important;
   right: 1px;
   cursor: pointer;
   z-index: 1;
}

../../styles/video-styles.scss

div.video-container {
    height: 100%;

    video,canvas {
        height: 100%;
        width: 100%;
        font-size: 0px;
        display: block;
        object-fit: fill;
    }

    canvas {
        transform: scaleX(-1);
    }
    video {
        /* border: 1px solid rgba(0,0,0,.5); */
    }

    div.interior-box {
        height: 100%;
        position: relative;
        border: 1px solid rgba(0,0,0,.5);
    }

    div.self {
        border: 1px solid #8ae010;
    }

    div.overlay {
        position: absolute;
        top: 1px;
        left: 1px;
        padding: 5px;
        background-color: rgba(53,53,53,.7);

        font-family: OpenSans;
        font-size: 16px;
        font-weight: 600;
        font-stretch: normal;
        font-style: normal;
        line-height: normal;
        letter-spacing: -0.24px;
        color: #ffffff;
    }
}

div.loading-blocker {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, .85);

    display: flex;
    justify-content: center;
    align-items: center;

    p {
        font-size: 24px;
        color: #777;
    }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""