From 0275a7b98ba7857dee949bbafbf966f90f1b5989 Mon Sep 17 00:00:00 2001 From: Raul Date: Tue, 17 Apr 2018 17:37:09 +0200 Subject: [PATCH] feat(controls): Add quality switcher Add quality switcher and Dash.js switcher #632 --- src/controls/controls.ts | 9 +- .../vg-quality-selector.spec.ts | 22 +++ .../vg-quality-selector.ts | 142 ++++++++++++++++++ src/core/core.ts | 9 ++ src/streaming/vg-dash/vg-dash.ts | 81 +++++++++- 5 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 src/controls/vg-quality-selector/vg-quality-selector.spec.ts create mode 100644 src/controls/vg-quality-selector/vg-quality-selector.ts diff --git a/src/controls/controls.ts b/src/controls/controls.ts index 2fa610f3..2e523885 100644 --- a/src/controls/controls.ts +++ b/src/controls/controls.ts @@ -12,7 +12,8 @@ import { VgScrubBarCuePoints } from './vg-scrub-bar/vg-scrub-bar-cue-points/vg-s import { VgScrubBarCurrentTime } from './vg-scrub-bar/vg-scrub-bar-current-time/vg-scrub-bar-current-time'; import { VgTimeDisplay, VgUtcPipe } from './vg-time-display/vg-time-display'; import { VgTrackSelector } from './vg-track-selector/vg-track-selector'; -import { VgControlsHidden } from './../core/services/vg-controls-hidden'; +import { VgControlsHidden } from '../core/services/vg-controls-hidden'; +import { VgQualitySelector } from './vg-quality-selector/vg-quality-selector'; @NgModule({ imports: [ CommonModule ], @@ -29,7 +30,8 @@ import { VgControlsHidden } from './../core/services/vg-controls-hidden'; VgScrubBarCurrentTime, VgTimeDisplay, VgUtcPipe, - VgTrackSelector + VgTrackSelector, + VgQualitySelector ], exports: [ VgControls, @@ -44,7 +46,8 @@ import { VgControlsHidden } from './../core/services/vg-controls-hidden'; VgScrubBarCurrentTime, VgTimeDisplay, VgUtcPipe, - VgTrackSelector + VgTrackSelector, + VgQualitySelector ], providers: [ VgControlsHidden ] }) diff --git a/src/controls/vg-quality-selector/vg-quality-selector.spec.ts b/src/controls/vg-quality-selector/vg-quality-selector.spec.ts new file mode 100644 index 00000000..ad940292 --- /dev/null +++ b/src/controls/vg-quality-selector/vg-quality-selector.spec.ts @@ -0,0 +1,22 @@ +import { VgQualitySelector } from "./vg-quality-selector"; +import { VgAPI } from "../../core/services/vg-api"; +import { ElementRef } from "@angular/core"; + +describe('Quality Selector control', () => { + let vgQualitySelector: VgQualitySelector; + + beforeEach(() => { + const ref: ElementRef = { + nativeElement: { + getAttribute: (name) => { + return name; + } + } + }; + vgQualitySelector = new VgQualitySelector(ref, new VgAPI()); + }); + + describe('onPlayerReady', () => { + + }); +}); diff --git a/src/controls/vg-quality-selector/vg-quality-selector.ts b/src/controls/vg-quality-selector/vg-quality-selector.ts new file mode 100644 index 00000000..33ac124a --- /dev/null +++ b/src/controls/vg-quality-selector/vg-quality-selector.ts @@ -0,0 +1,142 @@ +import { + Component, + ElementRef, + OnInit, + Input, + ViewEncapsulation, + OnDestroy, + SimpleChanges, + OnChanges, Output, EventEmitter +} from '@angular/core'; +import { VgAPI } from '../../core/services/vg-api'; +import { Subscription } from 'rxjs/Subscription'; +import { BitrateOption } from '../../core/core'; + +@Component({ + selector: 'vg-quality-selector', + encapsulation: ViewEncapsulation.None, + template: ` +
+
+ {{ bitrateSelected?.label }} +
+ + +
+ `, + styles: [ ` + vg-quality-selector { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + display: flex; + justify-content: center; + width: 50px; + height: 50px; + cursor: pointer; + color: white; + line-height: 50px; + } + vg-quality-selector .container { + position: relative; + display: flex; + flex-grow: 1; + align-items: center; + + padding: 0; + margin: 5px; + } + vg-quality-selector select.quality-selector { + width: 50px; + padding: 5px 8px; + border: none; + background: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + color: transparent; + font-size: 16px; + } + vg-quality-selector select.quality-selector::-ms-expand { + display: none; + } + vg-quality-selector select.quality-selector option { + color: #000; + } + vg-quality-selector .quality-selected { + position: absolute; + width: 100%; + height: 50px; + top: -6px; + text-align: center; + text-transform: uppercase; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + padding-top: 2px; + pointer-events: none; + } + vg-quality-selector .vg-icon-closed_caption:before { + width: 100%; + } + ` ] +}) +export class VgQualitySelector implements OnInit, OnChanges, OnDestroy { + @Input() vgFor: string; + @Input() bitrates: BitrateOption[]; + + @Output() onBitrateChange: EventEmitter = new EventEmitter(); + + bitrateSelected: BitrateOption; + + elem: HTMLElement; + target: any; + + subscriptions: Subscription[] = []; + + ariaValue: string; + + constructor(ref: ElementRef, public API: VgAPI) { + this.elem = ref.nativeElement; + } + + ngOnInit() { + if (this.API.isPlayerReady) { + this.onPlayerReady(); + } + else { + this.subscriptions.push(this.API.playerReadyEvent.subscribe(() => this.onPlayerReady())); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['bitrates'].currentValue && changes['bitrates'].currentValue.length) { + this.bitrates.forEach(item => item.label = item.label || Math.round(item.bitrate / 1000).toString()) + } + } + + onPlayerReady() { + this.target = this.API.getMediaById(this.vgFor); + } + + selectBitrate(index: number) { + this.bitrateSelected = this.bitrates[index]; + this.onBitrateChange.emit(this.bitrates[index]); + } + + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } +} diff --git a/src/core/core.ts b/src/core/core.ts index f65c882f..59bd07fc 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -29,6 +29,15 @@ export * from './vg-media/i-media-element'; // utility classes export * from './vg-media/vg-media-element'; +export interface BitrateOption { + qualityIndex: number; + width: number; + height: number; + bitrate: number; + mediaType: string; + label?: string; +} + /** * @internal */ diff --git a/src/streaming/vg-dash/vg-dash.ts b/src/streaming/vg-dash/vg-dash.ts index e1e2434d..55f76f12 100644 --- a/src/streaming/vg-dash/vg-dash.ts +++ b/src/streaming/vg-dash/vg-dash.ts @@ -1,17 +1,33 @@ -import { Directive, ElementRef, Input, SimpleChanges, OnChanges, OnDestroy, OnInit } from "@angular/core"; +import { + Directive, + ElementRef, + Input, + SimpleChanges, + OnChanges, + OnDestroy, + OnInit, + Output, + EventEmitter +} from "@angular/core"; import { VgAPI } from '../../core/services/vg-api'; import { Subscription } from 'rxjs/Subscription'; import { IDRMLicenseServer } from '../streaming'; +import { BitrateOption } from '../../core/core'; declare let dashjs; @Directive({ - selector: '[vgDash]' + selector: '[vgDash]', + exportAs: 'vgDash' }) export class VgDASH implements OnInit, OnChanges, OnDestroy { @Input() vgDash:string; @Input() vgDRMToken:string; @Input() vgDRMLicenseServer:IDRMLicenseServer; + @Input() vgDashBitrate: BitrateOption; + @Input() vgDashAuto = true; + + @Output() onGetBitrates: EventEmitter = new EventEmitter(); vgFor: string; target: any; @@ -37,12 +53,16 @@ export class VgDASH implements OnInit, OnChanges, OnDestroy { } ngOnChanges(changes:SimpleChanges) { - if (changes['vgDash'].currentValue) { + if (changes['vgDash'] && changes['vgDash'].currentValue) { this.createPlayer(); } else { this.destroyPlayer(); } + + if (changes['vgDashBitrate'] && changes['vgDashBitrate'].currentValue) { + this.setBitrate(changes['vgDashBitrate'].currentValue); + } } createPlayer() { @@ -72,6 +92,43 @@ export class VgDASH implements OnInit, OnChanges, OnDestroy { this.dash.initialize(this.ref.nativeElement); this.dash.setAutoPlay(false); + this.dash.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { + const audioList = this.dash.getBitrateInfoListFor('audio'); + const videoList = this.dash.getBitrateInfoListFor('video'); + + if (audioList.length > 1) { + if (this.vgDashAuto) { + audioList.forEach(item => item.qualityIndex = ++item.qualityIndex); + audioList.unshift({ + qualityIndex: 0, + width: 0, + height: 0, + bitrate: 0, + mediaType: 'video', + label: 'AUTO' + }); + } + + this.onGetBitrates.emit(audioList); + } + + if (videoList.length > 1) { + if (this.vgDashAuto) { + videoList.forEach(item => item.qualityIndex = ++item.qualityIndex); + videoList.unshift({ + qualityIndex: 0, + width: 0, + height: 0, + bitrate: 0, + mediaType: 'video', + label: 'AUTO' + }); + } + + this.onGetBitrates.emit(videoList); + } + }); + if (drmOptions) { this.dash.setProtectionData(drmOptions); } @@ -87,6 +144,24 @@ export class VgDASH implements OnInit, OnChanges, OnDestroy { } } + setBitrate(bitrate: BitrateOption) { + if (this.dash) { + if (bitrate.qualityIndex > 0) { + if (this.dash.getAutoSwitchQualityFor(bitrate.mediaType)) { + this.dash.setAutoSwitchQualityFor(bitrate.mediaType, false); + } + + if (this.vgDashAuto) { + this.dash.setQualityFor(bitrate.mediaType, bitrate.qualityIndex - 1); + } else { + this.dash.setQualityFor(bitrate.mediaType, bitrate.qualityIndex); + } + } else { + this.dash.setAutoSwitchQualityFor(bitrate.mediaType, true); + } + } + } + destroyPlayer() { if (this.dash) { this.dash.reset();