Commit e2bf6f13 authored by Mark Harding's avatar Mark Harding

(feat): introduces new player with support for the new transcoder backend

No related merge requests found
Pipeline #102370693 failed with stages
in 29 minutes and 44 seconds
......@@ -19,6 +19,7 @@
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
"node_modules/plyr/dist/plyr.css",
"node_modules/material-design-icons/iconfont/material-icons.css",
"src/main.css"
],
......
This diff is collapsed.
<div class="m-embed-video" *ngIf="object.subtype == 'video'">
<m-video
[autoplay]="false"
[muted]="false"
[src]="[{ 'uri': object.src['720.mp4'] }]"
<m-videoPlayer
[guid]="object.guid"
[playCount]="object['play:count']"
[poster]="object['thumbnail_src']"
></m-video>
[autoplay]="false"
[shouldPlayInModal]="false"
>
</m-videoPlayer>
</div>
......@@ -174,33 +174,12 @@
</span>
</div>
<m-video
width="100%"
height="300px"
controls="true"
muted="false"
[poster]="comment.custom_data.thumbnail_src"
[autoplay]="false"
[src]="[
{
res: '360',
uri:
'api/v1/media/' +
comment.custom_data.guid +
'/play?s=comment',
type: 'video/mp4'
}
]"
<m-videoPlayer
[guid]="comment.custom_data.guid"
[playCount]="comment['play:count']"
[torrent]="[
{ res: '360', key: comment.custom_data.guid + '/360.mp4' }
]"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
[autoplay]="false"
(mediaModalRequested)="openModal()"
>
</m-video>
></m-videoPlayer>
</div>
<!-- Custom type:: batch -->
......
......@@ -25,7 +25,6 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
import { MediaModalComponent } from '../../media/modal/modal.component';
import isMobile from '../../../helpers/is-mobile';
......@@ -82,7 +81,6 @@ export class CommentComponent implements OnChanges {
commentAge$: Observable<number>;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
......
......@@ -176,31 +176,12 @@
</span>
</div>
<m-video
width="100%"
height="300px"
style="background:#000;"
controls="true"
muted="false"
[poster]="comment.custom_data.thumbnail_src"
[autoplay]="false"
[src]="[
{
res: '360',
uri: 'api/v1/media/' + comment.custom_data.guid + '/play',
type: 'video/mp4'
}
]"
<m-videoPlayer
[guid]="comment.custom_data.guid"
[playCount]="comment['play:count']"
[torrent]="[
{ res: '360', key: comment.custom_data.guid + '/360.mp4' }
]"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
[autoplay]="false"
(mediaModalRequested)="openModal()"
>
</m-video>
></m-videoPlayer>
</div>
<!-- Custom type:: batch -->
......
......@@ -29,7 +29,6 @@ import { map } from 'rxjs/operators';
import { ActivityService } from '../../../common/services/activity.service';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
import { MediaModalComponent } from '../../media/modal/modal.component';
import isMobile from '../../../helpers/is-mobile';
......@@ -85,7 +84,6 @@ export class CommentComponentV2
canReply = true;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
......
......@@ -26,7 +26,7 @@ minds-activity {
}
.m-activity--video {
m-video {
m-videoPlayer {
@include m-theme() {
background-color: themed($m-black-always);
}
......
......@@ -296,22 +296,12 @@
</span>
</div>
<m-video
width="100%"
height="300px"
[muted]="false"
[poster]="activity.custom_data.thumbnail_src"
[src]="[{ 'res': '360', 'uri': 'api/v1/media/' + activity.custom_data.guid + '/play?s=activity', 'type': 'video/mp4' }]"
[guid]="activity.custom_data.guid"
[playCount]="activity['play:count']"
[torrent]="[{ res: '360', key: activity.custom_data.guid + '/360.mp4' }]"
<m-videoPlayer
[guid]="activity.entity_guid"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
[autoplay]="false"
(mediaModalRequested)="openModal()"
#player
>
<video-ads [player]="player" *ngIf="activity.monetized"></video-ads>
</m-video>
></m-videoPlayer>
</div>
<!-- Images -->
......
......@@ -19,7 +19,6 @@ import { OverlayModalService } from '../../../../../services/ux/overlay-modal';
import { MediaModalComponent } from '../../../../media/modal/modal.component';
import { BoostCreatorComponent } from '../../../../boost/creator/creator.component';
import { WireCreatorComponent } from '../../../../wire/creator/creator.component';
import { MindsVideoComponent } from '../../../../media/components/video/video.component';
import { EntitiesService } from '../../../../../common/services/entities.service';
import { Router } from '@angular/router';
import { BlockListService } from '../../../../../common/services/block-list.service';
......@@ -30,6 +29,7 @@ import { AutocompleteSuggestionsService } from '../../../../suggestions/services
import { ActivityService } from '../../../../../common/services/activity.service';
import { FeaturesService } from '../../../../../services/features.service';
import isMobile from '../../../../../helpers/is-mobile';
import { MindsVideoPlayerComponent } from '../../../../media/components/video-player/player.component';
@Component({
moduleId: module.id,
......@@ -153,7 +153,7 @@ export class Activity implements OnInit {
}
}
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('player', { static: false }) player: MindsVideoPlayerComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
protected time_created: any;
......
import {
trigger,
state,
style,
transition,
animate,
} from '@angular/animations';
export const PLAYER_ANIMATIONS = [
trigger('fadeAnimation', [
state(
'in',
style({
visibility: 'visible',
opacity: 1,
})
),
state(
'out',
style({
visibility: 'hidden',
opacity: 0,
})
),
transition('in <=> out', [animate('300ms ease-in')]),
]),
];
<ng-container *ngIf="isPlayable(); else placeholder">
<span class="m-videoPlayer__notice--error" *ngIf="status === 'failed'">
There was an error transcoding this video.
</span>
<span
class="m-videoPlayer__notice--transcoding"
*ngIf="status === 'transcoding'"
>
This video is still transcoding.
</span>
<span class="m-videoPlayer__notice--created" *ngIf="status === 'created'">
This video is waiting to be transcoded.
</span>
<div class="m-videoPlayer__screen">
<plyr
style="display: block; width: 100%; height: 100%;"
[plyrPoster]="poster"
[plyrPlaysInline]="true"
[plyrSources]="sources"
[plyrOptions]="options"
(plyrPlay)="onPlayed($event)"
(plyrEnterFullScreen)="fullScreenChange.next($event)"
(plyrExitFullScreen)="fullScreenChange.next($event)"
>
</plyr>
</div>
</ng-container>
<ng-template #placeholder>
<div
class="m-videoPlayer__placeholder"
[style.background-image]="'url(' + poster + ')'"
>
<i
class="material-icons minds-video-play-icon"
(click)="onPlaceholderClick($event)"
>play_circle_outline</i
>
</div>
</ng-template>
.plyr video {
height: 100%;
}
[class*='m-videoPlayer__notice'] {
display: block;
height: auto;
line-height: normal;
padding: 16px;
position: absolute;
z-index: 1;
width: 100%;
box-sizing: border-box;
@include m-theme() {
color: themed($m-white);
@include m-theme() {
background: linear-gradient(
rgba(themed($m-black-always), 0.5),
rgba(themed($m-black-always), 0)
);
color: themed($m-white-always);
}
}
}
m-videoPlayer {
display: block;
position: relative;
width: 100%;
}
.m-videoPlayer__placeholder {
height: 330px;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-color: #222;
position: relative;
i {
opacity: 0.8;
display: block;
text-align: center;
top: 50%;
transform: translateY(-50%);
font-size: 100px;
position: absolute;
cursor: pointer;
width: 100%;
transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
color: themed($m-white-always);
text-shadow: 0 0 3px rgba(themed($m-black-always), 0.6);
}
}
&:hover {
i {
opacity: 1;
}
}
}
import {
Component,
OnDestroy,
OnInit,
Input,
ViewChild,
Output,
EventEmitter,
ChangeDetectorRef,
} from '@angular/core';
import { PLAYER_ANIMATIONS } from './player.animations';
import { VideoPlayerService, VideoSource } from './player.service';
import isMobile from '../../../../helpers/is-mobile';
import Plyr from 'plyr';
import { PlyrComponent } from 'ngx-plyr';
@Component({
selector: 'm-videoPlayer',
templateUrl: 'player.component.html',
animations: PLAYER_ANIMATIONS,
providers: [VideoPlayerService],
})
export class MindsVideoPlayerComponent implements OnInit, OnDestroy {
/**
* MH: dislike having to emit an event to open modal, but this is
* the quickest work around for now
*/
@Output() mediaModalRequested: EventEmitter<void> = new EventEmitter();
/**
* Modal needs to know if we have left full screen
*/
@Output() fullScreenChange: EventEmitter<Event> = new EventEmitter();
/**
* This is the video player component
*/
@ViewChild(PlyrComponent, { static: false }) player: PlyrComponent;
/**
* Options for Plyr to use
*/
options: Plyr.Options = {
controls: [
'play-large',
'play',
'progress',
'current-time',
'mute',
'volume',
'captions',
'settings',
'airplay',
'fullscreen',
],
};
constructor(
private service: VideoPlayerService,
private cd: ChangeDetectorRef
) {}
ngOnInit(): void {
this.service.load().then(() => {
this.cd.markForCheck();
this.cd.detectChanges();
});
}
ngOnDestroy(): void {}
@Input('guid')
set guid(guid: string) {
this.service.setGuid(guid);
}
@Input('autoplay')
set autoplay(autoplay: boolean) {
this.options.autoplay = autoplay;
}
@Input('isModal')
set isModal(isModal: boolean) {
this.service.setIsModal(isModal);
}
@Input('shouldPlayInModal')
set shouldPlayInModal(shouldPlayInModal: boolean) {
this.service.setShouldPlayInModal(shouldPlayInModal);
}
get poster(): string {
return this.service.poster;
}
get sources(): Plyr.Source[] {
return this.service.sources;
}
get status(): string {
return this.service.status;
}
onPlayed(event: Plyr.PlyrEvent): void {
// console.log('played', event);
}
/**
* If the component is in a playable state
* @return boolean
*/
isPlayable(): boolean {
return this.service.isPlayable();
}
/**
* Placeholder clicked
* @param e
* @return void
*/
onPlaceholderClick(e: MouseEvent): void {
// If we have a player, then play
if (this.player) {
this.player.player.play();
return;
}
// Play in modal if required
if (this.service.shouldPlayInModal) {
return this.mediaModalRequested.next();
}
console.error('Placeholder was clicked but we have no action to take');
}
/**
* Pause the player, if there is one
* @return void
*/
pause(): void {
if (this.player) {
this.player.player.pause();
return;
}
}
}
import { Injectable } from '@angular/core';
import { Client } from '../../../../services/api';
import isMobile from '../../../../helpers/is-mobile';
export type VideoSource = {
id: string;
type: string;
size: number;
src: string;
};
@Injectable()
export class VideoPlayerService {
/**
* @var string
*/
guid: string;
/**
* @var VideoSource[]
*/
sources: VideoSource[];
/**
* @var string
*/
status: string;
/**
* A poster is thumbnail
* @var string
*/
poster: string;
/**
* False would be inline
* @var boolean
*/
shouldPlayInModal = true;
/**
* If its a modal or not
* @var boolean
*/
isModal = false;
constructor(private client: Client) {}
/**
* Set the guid that we are interacting with
* @param string guid
* @return VideoPlayerService
*/
setGuid(guid: string): VideoPlayerService {
this.guid = guid;
return this;
}
/**
* Set the guid that we are interacting with
* @param boolean is
* @return VideoPlayerService
*/
setIsModal(isModal: boolean): VideoPlayerService {
this.isModal = isModal;
return this;
}
setShouldPlayInModal(shouldPlayInModal: boolean): VideoPlayerService {
this.shouldPlayInModal = shouldPlayInModal;
return this;
}
/**
* Return the sources for a video
* @return Promise<void>
*/
async load(): Promise<void> {
try {
let response = await this.client.get('api/v2/media/video/' + this.guid);
this.sources = (<any>response).sources;
this.poster = (<any>response).poster;
this.status = (<any>response).transcode_status;
} catch (e) {
console.error(e);
}
}
/**
* @return boolean
*/
private canPlayInModal(): boolean {
const isNotTablet: boolean = Math.min(screen.width, screen.height) < 768;
return isMobile() && isNotTablet;
}
/**
* Returns if the video is able to be played
* @return boolean
*/
isPlayable(): boolean {
return (
this.isModal || // Always playable in modal
!this.shouldPlayInModal || // Equivalent of asking to play inline
(this.canPlayInModal() && !this.isModal)
); // We can play in the modal and this isn't a modal
}
/**
* Record play
*/
async recordPlay(): Promise<void> {
// TODO
}
}
import { Component, Input, ElementRef } from '@angular/core';
import { VideoAdsService } from './ads.service';
@Component({
selector: 'video-ads',
template: ``,
})
export class VideoAds {
service: VideoAdsService = new VideoAdsService();
@Input() player;
adContainer;
adLoader;
adManager;
initialized: boolean = false;
google = window.google;
constructor(private element: ElementRef) {}
ngOnInit() {
//this.setupIMA();
this.element.nativeElement.style.display = 'none';
}
setupIMA() {
this.adContainer = new this.google.ima.AdDisplayContainer(
this.element.nativeElement,
this.player.element
);
this.adLoader = new this.google.ima.AdsLoader(this.adContainer);
// Listen and respond to ads loaded and error events.
this.adLoader.addEventListener(
this.google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
this.onLoaded.bind(this),
false
);
this.adLoader.addEventListener(
this.google.ima.AdErrorEvent.Type.AD_ERROR,
this.onError.bind(this),
false
);
// Request video ads.
var adsRequest = new this.google.ima.AdsRequest();
adsRequest.adTagUrl =
'https://pubads.g.doubleclick.net/gampad/ads? ' +
'sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&' +
'impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&' +
'cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=';
adsRequest.linearAdSlotWidth = this.player.element.clientWidth;
adsRequest.linearAdSlotHeight = this.player.element.clientHeight;
adsRequest.nonLinearAdSlotWidth = this.player.element.clientWidth;
adsRequest.nonLinearAdSlotHeight = 150;
adsRequest.setAdWillAutoPlay(true);
this.adLoader.requestAds(adsRequest);
}
onLoaded(e) {
let settings = new this.google.ima.AdsRenderingSettings();
settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
// videoContent should be set to the content video element.
this.adManager = e.getAdsManager(this.player.element, settings);
// Add listeners to the required events.
this.adManager.addEventListener(
this.google.ima.AdErrorEvent.Type.AD_ERROR,
this.onError.bind(this)
);
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
this.onPause.bind(this)
);
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
this.onResume.bind(this)
);
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
this.onEvent.bind(this)
);
// Listen to any additional events, if necessary.
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.LOADED,
this.onEvent.bind(this)
);
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.STARTED,
this.onEvent.bind(this)
);
this.adManager.addEventListener(
this.google.ima.AdEvent.Type.COMPLETE,
this.onEvent.bind(this)
);
var initWidth = this.player.element.clientWidth;
var initHeight = this.player.element.clientHeight;
//this.adManagerDimensions.width = initWidth;
//this.adsManagerDimensions.height = initHeight;
this.adManager.init(initWidth, initHeight, this.google.ima.ViewMode.NORMAL);
this.adManager.resize(
initWidth,
initHeight,
this.google.ima.ViewMode.NORMAL
);
if (!this.player.muted) {
this.playAds();
} else {
this.player.element.addEventListener(
'volumechange',
this.playAds.bind(this)
);
}
}
playAds() {
if (this.initialized) return;
this.initialized = true;
this.element.nativeElement.style.display = 'block';
this.player.autoplay = true;
this.adContainer.initialize();
try {
// Initialize the ads manager. Ad rules playlist will start at this time.
this.adManager.init(640, 360, this.google.ima.ViewMode.NORMAL);
// Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules.
this.adManager.start();
} catch (err) {
// An error may be thrown if there was a problem with the VAST response.
//videoContent.play();
console.log(err);
this.element.nativeElement.style.display = 'none';
return false;
}
return true;
}
onEvent(e) {
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
// don't have ad object associated.
var ad = e.getAd();
switch (e.type) {
case this.google.ima.AdEvent.Type.LOADED:
if (!ad.isLinear()) {
// Position AdDisplayContainer correctly for overlay.
// Use ad.width and ad.height.
//this.player.nativeElement.play();
}
break;
case this.google.ima.AdEvent.Type.STARTED:
// This event indicates the ad has started - the video player
// can adjust the UI, for example display a pause button and
// remaining time.
//if (ad.isLinear()) {
//}
break;
case this.google.ima.AdEvent.Type.COMPLETE:
//if (ad.isLinear()) {
//}
this.element.nativeElement.style.display = 'none';
break;
}
}
onPause(e) {
this.element.nativeElement.style.display = 'block';
this.player.element.pause();
}
onResume(e) {
this.player.element.play();
this.element.nativeElement.style.display = 'none';
}
onError(e) {
console.log(e.getError());
this.adManager.destroy();
}
ngOnDestroy() {
if (this.adManager) this.adManager.destroy();
}
}
import { Directive, Input } from '@angular/core';
import { VideoAdsService } from './ads.service';
@Directive({
selector: '[videoAds]',
})
export class VideoAdsDirective {
@Input() autoplay: boolean = true;
@Input() muted: boolean = false;
service: VideoAdsService = new VideoAdsService();
ngOnInit() {
if (this.autoplay && !this.muted) {
//load the service
}
}
}
video-ads {
position: absolute;
display: block;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
z-index: 111111;
> div,
iframe {
position: absolute !important;
width: 100% !important;
height: 100% !important;
top: 0 !important;
left: 0 !important;
}
}
export class VideoAdsService {}
<video
[src]="src"
[autoplay]="autoplay"
[poster]="poster"
[muted]="muted"
preload="none"
allowfullscreen
#player
></video>
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { MindsPlayerInterface } from './player.interface';
@Component({
moduleId: module.id,
selector: 'm-video--direct-http-player',
templateUrl: 'direct-http.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MindsVideoDirectHttpPlayer
implements OnInit, OnDestroy, MindsPlayerInterface {
@ViewChild('player', { static: true }) player: ElementRef;
@Input() muted: boolean = false;
@Input() poster: string = '';
@Input() autoplay: boolean = false;
@Input() guid: string | number;
src: string;
@Input('src') set _src(src: string) {
this.src = src;
const player = this.getPlayer();
if (player) {
this.loading = true;
this.detectChanges();
player.load();
}
}
@Output() onPlay: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onError: EventEmitter<{
player: HTMLVideoElement;
e;
}> = new EventEmitter();
@Output() onCanPlayThrough: EventEmitter<any> = new EventEmitter();
@Output() onLoadedMetadata: EventEmitter<any> = new EventEmitter();
loading: boolean = false;
constructor(protected cd: ChangeDetectorRef) {}
protected _emitPlay = () => this.onPlay.emit(this.getPlayer());
protected _emitPause = () => this.onPause.emit(this.getPlayer());
protected _emitEnd = () => this.onEnd.emit(this.getPlayer());
protected _emitError = e =>
this.onError.emit({ player: this.getPlayer(), e });
protected _emitCanPlayThrough = () =>
this.onCanPlayThrough.emit(this.getPlayer());
protected _emitLoadedMetadata = () =>
this.onLoadedMetadata.emit(this.getPlayer());
protected _canPlayThrough = () => {
this.loading = false;
this.detectChanges();
this._emitCanPlayThrough();
};
protected _onPlayerError = e => {
if (
!e.target.error &&
e.target.networkState !== HTMLMediaElement.NETWORK_NO_SOURCE
) {
// Poster error
return;
}
this.loading = false;
this.detectChanges();
this._emitError(e);
};
ngOnInit() {
const player = this.getPlayer();
player.addEventListener('playing', this._emitPlay);
player.addEventListener('pause', this._emitPause);
player.addEventListener('ended', this._emitEnd);
player.addEventListener('error', this._onPlayerError);
player.addEventListener('canplaythrough', this._canPlayThrough);
player.addEventListener('loadedmetadata', this._emitLoadedMetadata);
this.loading = true;
}
ngOnDestroy() {
const player = this.getPlayer();
if (player) {
player.removeEventListener('playing', this._emitPlay);
player.removeEventListener('pause', this._emitPause);
player.removeEventListener('ended', this._emitEnd);
player.removeEventListener('error', this._onPlayerError);
player.removeEventListener('canplaythrough', this._canPlayThrough);
player.removeEventListener('loadedmetadata', this._emitLoadedMetadata);
}
}
getPlayer(): HTMLVideoElement {
return this.player.nativeElement;
}
async play() {
const player = this.getPlayer();
try {
await player.play();
} catch (e) {
console.log(e);
}
}
pause() {
const player = this.getPlayer();
try {
player.pause();
} catch (e) {
console.log(e);
}
}
async toggle() {
const player = this.getPlayer();
if (player.paused) {
await this.play();
} else {
this.pause();
}
}
resumeFromTime(time: number = 0) {
// TODO detect if it's still transcoding
const player = this.getPlayer();
try {
player.currentTime = time;
this.play();
} catch (e) {
console.log(e);
}
}
isPlaying() {
const player = this.getPlayer();
return !player.paused;
}
isLoading() {
return this.loading;
}
requestFullScreen() {
const player: any = this.getPlayer();
if (player.requestFullscreen) {
player.requestFullscreen();
} else if (player.msRequestFullscreen) {
player.msRequestFullscreen();
} else if (player.mozRequestFullScreen) {
player.mozRequestFullScreen();
} else if (player.webkitRequestFullscreen) {
player.webkitRequestFullscreen();
}
}
getInfo() {
return {};
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
import { EventEmitter } from '@angular/core';
export interface MindsPlayerInterface {
muted: boolean;
poster: string;
autoplay: boolean;
src: string;
onPlay: EventEmitter<HTMLVideoElement>;
onPause: EventEmitter<HTMLVideoElement>;
onEnd: EventEmitter<HTMLVideoElement>;
onError: EventEmitter<{ player: HTMLVideoElement; e }>;
getPlayer(): HTMLVideoElement;
play(): void;
pause(): void;
toggle(): void;
resumeFromTime(time: number);
isLoading(): boolean;
isPlaying(): boolean;
requestFullScreen(): void;
getInfo(): any;
}
<video
[poster]="poster"
[muted]="muted"
preload="none"
allowfullscreen
#player
></video>
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { MindsPlayerInterface } from './player.interface';
import { WebtorrentService } from '../../../../webtorrent/webtorrent.service';
import { Client } from '../../../../../services/api/client';
import base64ToBlob from '../../../../../helpers/base64-to-blob';
@Component({
moduleId: module.id,
selector: 'm-video--torrent-player',
templateUrl: 'torrent.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MindsVideoTorrentPlayer
implements OnInit, AfterViewInit, OnDestroy, MindsPlayerInterface {
@ViewChild('player', { static: true }) player: ElementRef;
@Input() muted: boolean = false;
@Input() poster: string = '';
@Input() autoplay: boolean = false;
@Input() guid: string | number;
src: string;
@Input('src') set _src(src: string) {
this.src = src;
if (this.initialized) {
this.removeTorrent();
setTimeout(() => {
this.loadTorrent();
}, 0);
}
}
@Output() onPlay: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onError: EventEmitter<{ player; e }> = new EventEmitter();
@Output() onCanPlayThrough: EventEmitter<any> = new EventEmitter();
@Output() onLoadedMetadata: EventEmitter<any> = new EventEmitter();
initialized: boolean = false;
loading: boolean = false;
isModal: boolean = false;
protected torrentId: string;
protected torrentReady: boolean = false;
protected torrentInfo = {
progress: 0,
peers: 0,
ul: 0,
dl: 0,
ulspeed: 0,
dlspeed: 0,
};
protected infoTimer$: any;
protected deferredResumeFromTime: number;
constructor(
protected cd: ChangeDetectorRef,
protected client: Client,
protected webtorrent: WebtorrentService
) {}
protected _emitPlay = () => this.onPlay.emit(this.getPlayer());
protected _emitPause = () => this.onPause.emit(this.getPlayer());
protected _emitEnd = () => this.onEnd.emit(this.getPlayer());
protected _emitError = e =>
this.onError.emit({ player: this.getPlayer(), e });
protected _emitCanPlayThrough = () =>
this.onCanPlayThrough.emit(this.getPlayer());
protected _emitLoadedMetadata = () =>
this.onLoadedMetadata.emit(this.getPlayer());
protected _canPlayThrough = () => {
this.loading = false;
this.detectChanges();
this._emitCanPlayThrough();
};
protected _onError = e => {
this.loading = false;
this.detectChanges();
this._emitError(e);
};
protected _onPlayerError = e => {
if (!e.target.error) {
// Poster error
return;
}
this.loading = false;
this.detectChanges();
this._emitError(e);
};
protected _refreshInfo = () => {
if (
!this.torrentId ||
!this.torrentReady ||
!this.webtorrent.get(this.torrentId)
) {
this.torrentInfo = {
progress: 0,
peers: 0,
ul: 0,
dl: 0,
ulspeed: 0,
dlspeed: 0,
};
} else {
const torrent = this.webtorrent.get(this.torrentId);
this.torrentInfo = {
progress: torrent.progress,
peers: torrent.numPeers,
ul: torrent.uploaded,
dl: torrent.downloaded,
ulspeed: torrent.uploadSpeed,
dlspeed: torrent.downloadSpeed,
};
}
this.detectChanges();
};
ngOnInit() {
const player = this.getPlayer();
player.addEventListener('playing', this._emitPlay);
player.addEventListener('pause', this._emitPause);
player.addEventListener('ended', this._emitEnd);
player.addEventListener('error', this._onPlayerError);
player.addEventListener('canplaythrough', this._canPlayThrough);
player.addEventListener('loadedmetadata', this._emitLoadedMetadata);
this.infoTimer$ = setInterval(this._refreshInfo, 1000);
this.isModal = document.body.classList.contains('m-overlay-modal--shown');
}
ngAfterViewInit() {
this.initialized = true;
if (this.autoplay) {
this.play();
}
}
ngOnDestroy() {
if (this.infoTimer$) {
clearInterval(this.infoTimer$);
}
this.removeTorrent();
const player = this.getPlayer();
if (player) {
player.removeEventListener('playing', this._emitPlay);
player.removeEventListener('pause', this._emitPause);
player.removeEventListener('ended', this._emitEnd);
player.removeEventListener('error', this._onPlayerError);
player.removeEventListener('canplaythrough', this._canPlayThrough);
player.removeEventListener('loadedmetadata', this._emitLoadedMetadata);
}
}
getPlayer(): HTMLVideoElement {
return this.player.nativeElement;
}
play() {
const player = this.getPlayer();
try {
if (this.torrentReady) {
player.play();
} else {
this.loadTorrent();
}
} catch (e) {
console.log(e);
}
}
pause() {
const player = this.getPlayer();
try {
player.pause();
} catch (e) {
console.log(e);
}
}
toggle() {
const player = this.getPlayer();
if (player.paused) {
this.play();
} else {
this.pause();
}
}
resumeFromTime(time: number = 0) {
// TODO detect if it's still transcoding
const player = this.getPlayer();
try {
if (this.torrentReady) {
player.currentTime = time;
this.play();
} else {
this.deferredResumeFromTime = time;
if (!this.loading) {
this.loadTorrent();
}
}
} catch (e) {
console.log(e);
}
}
isPlaying() {
const player = this.getPlayer();
return !player.paused;
}
isLoading() {
return this.loading;
}
requestFullScreen() {
const player: any = this.getPlayer();
if (player.requestFullscreen) {
player.requestFullscreen();
} else if (player.msRequestFullscreen) {
player.msRequestFullscreen();
} else if (player.mozRequestFullScreen) {
player.mozRequestFullScreen();
} else if (player.webkitRequestFullscreen) {
player.webkitRequestFullscreen();
}
}
getInfo() {
return this.torrentInfo;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
//
async loadTorrent() {
if (this.loading) {
return;
} else if (this.torrentReady) {
this.play();
return;
}
this.removeTorrent();
this.loading = true;
this.torrentReady = false;
this.detectChanges();
let torrentFile;
let infoHash;
let webSeed;
try {
const response: any = await this.client.get(
`api/v2/media/magnet/${this.src}`
);
torrentFile = base64ToBlob(response.encodedTorrent);
infoHash = response.infoHash;
webSeed = response.httpSrc;
} catch (e) {
this.loading = false;
this.detectChanges();
console.error('[TorrentVideo] Magnet download', e);
this._emitError(e);
return;
}
try {
this.torrentId = infoHash;
const torrent = await this.webtorrent.add(torrentFile, infoHash);
if (webSeed) {
torrent.addWebSeed(webSeed);
}
this.loading = false;
this.detectChanges();
const file = torrent.files.find(file => file.name.endsWith('.mp4'));
if (!file) {
this.loading = false;
this.detectChanges();
this.webtorrent.remove(this.torrentId);
this.torrentId = void 0;
console.error('[TorrentVideo] Video file not found');
this._emitError('E_NO_FILE');
return;
}
file.renderTo(this.getPlayer(), err => {
if (err) {
this.loading = false;
this.detectChanges();
this.webtorrent.remove(this.torrentId);
this.torrentId = void 0;
console.error('[TorrentVideo] Video render', err);
this._emitError(err);
return;
}
this.loading = false;
this.torrentReady = true;
this.detectChanges();
if (typeof this.deferredResumeFromTime) {
const time = this.deferredResumeFromTime;
this.deferredResumeFromTime = void 0;
this.resumeFromTime(time);
}
});
} catch (e) {
this.loading = false;
this.detectChanges();
console.error('[TorrentVideo] Webtorrent general error', e);
this._emitError(e);
}
}
removeTorrent() {
if (this.torrentId) {
this.webtorrent.remove(this.torrentId);
this.torrentId = void 0;
this.loading = false;
this.torrentReady = false;
}
}
}
<span id="seeker" class="progress-bar" (click)="seek($event)">
<div class="minds-bar progress" [ngStyle]="{ width: seeked + '%' }"></div>
<div class="seeker-ball" [ngStyle]="{ left: seeked + '%' }"></div>
<div class="minds-bar total"></div>
</span>
<span class="progress-stamps"
>{{ elapsed.minutes }}:{{ elapsed.seconds }}/{{ time.minutes }}:{{
time.seconds
}}</span
>
m-video--progress-bar {
flex: 1;
display: flex;
padding: 12px;
.progress-bar {
flex: 1;
position: relative;
.minds-bar {
position: absolute;
left: 0px;
height: 2px;
vertical-align: middle;
border-radius: 2px;
margin-top: 10px;
@include m-theme() {
background-color: themed($m-white-always);
}
&.total {
width: 100%;
@include m-theme() {
background-color: rgba(themed($m-white-always), 0.5);
}
}
}
}
.progress-stamps {
font-size: 12px;
font-weight: 600;
font-family: 'Roboto', Helvetica, sans-serif;
padding: 0 12px;
line-height: 24px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
cursor: default;
}
.m-play-count {
text-align: right;
> i {
vertical-align: middle;
margin-right: 0.15em;
}
> span {
vertical-align: middle;
font-size: 12px;
font-weight: 300;
}
}
.seeker-ball {
position: absolute;
z-index: 999;
border-radius: 30px;
top: 7px;
left: 0;
width: 1em;
cursor: pointer;
height: 1em;
margin: -0.2em 0px 0px -0.5em;
width: 14px;
height: 14px;
padding: 0px;
-webkit-transition: -webkit-transform 0.15s ease-in-out;
transition: transform 0.15s ease-in-out;
@include m-theme() {
background-color: themed($m-white-always);
}
}
}
///<reference path="../../../../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
Component,
DebugElement,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule as NgCommonModule } from '@angular/common';
import { MindsVideoProgressBar } from './progress-bar.component';
import { MindsVideoDirectHttpPlayer } from '../players/direct-http.component';
import { MindsPlayerInterface } from '../players/player.interface';
class MindsVideoDirectHttpPlayerMock implements MindsPlayerInterface {
@Input() muted: boolean;
@Input() poster: string;
@Input() autoplay: boolean;
@Input() src: string;
@Output() onPlay: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onError: EventEmitter<{
player: HTMLVideoElement;
e;
}> = new EventEmitter();
getPlayer = (): HTMLVideoElement => {
return null;
};
play = () => {};
pause = () => {};
toggle = () => {};
resumeFromTime = () => {};
isLoading = (): boolean => {
return false;
};
isPlaying = (): boolean => {
return false;
};
requestFullScreen = jasmine.createSpy('requestFullScreen').and.stub();
getInfo = () => {};
}
describe('MindsVideoProgressBar', () => {
let comp: MindsVideoProgressBar;
let fixture: ComponentFixture<MindsVideoProgressBar>;
let window: any = {};
let e: Event;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MindsVideoProgressBar], // declare the test component
imports: [FormsModule, RouterTestingModule, NgCommonModule],
}).compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(done => {
window.removeEventListener = () => {};
window.addEventListener = () => {};
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(MindsVideoProgressBar);
comp = fixture.componentInstance;
const video = document.createElement('video');
video.src = 'thisisavideo.mp4';
comp.element = video;
const playerRef = new MindsVideoDirectHttpPlayerMock();
playerRef.getPlayer = () => {
return video;
};
comp.playerRef = playerRef;
spyOn(window, 'removeEventListener').and.stub();
spyOn(window, 'addEventListener').and.stub();
spyOn(comp.element, 'addEventListener').and.stub();
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
// it('should have a Play icon and a Control bar', () => {
// const seeker = fixture.debugElement.query(By.css('#seeker'));
// const seekerBall = fixture.debugElement.query(By.css('.seeker-ball'));
// const stamps = fixture.debugElement.query(By.css('.progress-stamps'));
// expect(seeker).not.toBeNull();
// expect(seekerBall).not.toBeNull();
// expect(stamps).not.toBeNull();
// });
it('time is properly calculated', () => {
comp.duration = 111;
comp.calculateTime();
fixture.detectChanges();
expect(comp.time).toEqual({ minutes: '01', seconds: 51 });
});
it('time is properly calculated', () => {
comp.duration = 113;
comp.element.currentTime = 111;
comp.calculateElapsed();
fixture.detectChanges();
expect(comp.time).toEqual({ minutes: '00', seconds: '00' });
});
it('time is properly calculated', () => {
comp.duration = 9;
comp.calculateTime();
fixture.detectChanges();
expect(comp.time).toEqual({ minutes: '00', seconds: '09' });
});
it('time is properly calculated', () => {
comp.duration = 9;
comp.element.currentTime = 9;
comp.calculateElapsed();
fixture.detectChanges();
expect(comp.time).toEqual({ minutes: '00', seconds: '00' });
});
it('call play on togglepause', () => {
spyOn(comp.element, 'play').and.callThrough();
comp.togglePause();
fixture.detectChanges();
expect(comp.element.play).toHaveBeenCalled();
});
it('moves over time depending on the event', () => {
comp.element.currentTime = 11;
fixture.detectChanges();
comp.moveToTime(2);
fixture.detectChanges();
expect(comp.element.currentTime).toBe(13);
});
it('execute control should call controls', () => {
comp.element.currentTime = 11;
fixture.detectChanges();
let e: any = {};
e.preventDefault = () => {};
e.keyCode = 37;
comp.executeControl(e);
fixture.detectChanges();
expect(comp.element.currentTime).toBe(9);
});
it('execute control should call controls', () => {
comp.element.currentTime = 11;
fixture.detectChanges();
let e: any = {};
e.preventDefault = () => {};
e.keyCode = 39;
comp.executeControl(e);
fixture.detectChanges();
expect(comp.element.currentTime).toBe(13);
});
it('execute control should call controls', () => {
comp.element.currentTime = 11;
spyOn(comp.element, 'play').and.callThrough();
fixture.detectChanges();
let e: any = {};
e.preventDefault = () => {};
e.keyCode = 32;
comp.executeControl(e);
fixture.detectChanges();
expect(comp.element.play).toHaveBeenCalled();
});
it('should calculate remaining', () => {
comp.element.currentTime = 11;
comp.duration = 111;
fixture.detectChanges();
comp.calculateRemaining();
fixture.detectChanges();
expect(comp.remaining).toBeNull();
});
it('should calculate remaining', () => {
comp.element.currentTime = 3;
comp.duration = 111;
fixture.detectChanges();
comp.calculateRemaining();
fixture.detectChanges();
expect(comp.elapsed).toEqual({ minutes: '00', seconds: '00' });
});
it('should calculate elapsed', () => {
comp.element.currentTime = 11;
comp.duration = 111;
fixture.detectChanges();
comp.calculateElapsed();
fixture.detectChanges();
expect(comp.elapsed).toEqual({ minutes: '00', seconds: 11 });
});
});
import {
Component,
Input,
ElementRef,
ChangeDetectorRef,
OnDestroy,
OnInit,
} from '@angular/core';
import { MindsPlayerInterface } from '../players/player.interface';
@Component({
selector: 'm-video--progress-bar',
templateUrl: 'progress-bar.component.html',
})
export class MindsVideoProgressBar implements OnInit, OnDestroy {
@Input('player') playerRef: MindsPlayerInterface;
element: HTMLVideoElement;
time: { minutes: any; seconds: any } = {
minutes: '00',
seconds: '00',
};
elapsed: { minutes: any; seconds: any } = {
minutes: '00',
seconds: '00',
};
remaining: { minutes: any; seconds: any } | null = null;
seek_interval;
seeked: number = 0;
keyPressListener: any;
duration: number = 0;
constructor(private cd: ChangeDetectorRef, public _element: ElementRef) {}
protected _loadedMetadata = () => {
this.duration = this.element.duration;
this.calculateTime();
};
ngOnInit() {
this.keyPressListener = this.executeControl.bind(this);
this.bindToElement();
}
bindToElement() {
if (this.element) {
this.element.removeEventListener('loadedmetadata', this._loadedMetadata);
}
if (this.playerRef.getPlayer()) {
this.element = this.playerRef.getPlayer();
this.element.addEventListener('loadedmetadata', this._loadedMetadata);
if (this.element.readyState > 0) {
this._loadedMetadata();
}
}
}
ngOnDestroy() {
this.element.removeEventListener('loadedmetadata', this._loadedMetadata);
clearInterval(this.seek_interval);
}
calculateTime() {
const seconds = this.duration;
this.time.minutes = Math.floor(seconds / 60);
if (parseInt(this.time.minutes) < 10)
this.time.minutes = '0' + this.time.minutes;
this.time.seconds = Math.floor(seconds % 60);
if (parseInt(this.time.seconds) < 10)
this.time.seconds = '0' + this.time.seconds;
}
calculateElapsed() {
const seconds = this.element.currentTime;
this.elapsed.minutes = Math.floor(seconds / 60);
if (parseInt(this.elapsed.minutes) < 10)
this.elapsed.minutes = '0' + this.elapsed.minutes;
this.elapsed.seconds = Math.floor(seconds % 60);
if (parseInt(this.elapsed.seconds) < 10)
this.elapsed.seconds = '0' + this.elapsed.seconds;
}
calculateRemaining() {
if (!this.duration || this.element.paused) {
this.remaining = null;
return;
}
const seconds = this.duration - this.element.currentTime;
this.remaining = { seconds: 0, minutes: 0 };
this.remaining.minutes = Math.floor(seconds / 60);
if (parseInt(this.remaining.minutes) < 10)
this.remaining.minutes = '0' + this.remaining.minutes;
this.remaining.seconds = Math.floor(seconds % 60);
if (parseInt(this.remaining.seconds) < 10)
this.remaining.seconds = '0' + this.remaining.seconds;
}
seek(e) {
e.preventDefault();
const seeker = e.target;
const seek = e.offsetX / seeker.offsetWidth;
this.element.currentTime = this.seekerToSeconds(seek);
}
seekerToSeconds(seek) {
const duration = this.element.duration;
return duration * seek;
}
getSeeker() {
if (this.seek_interval) clearInterval(this.seek_interval);
this.seek_interval = setInterval(() => {
this.seeked = (this.element.currentTime / this.element.duration) * 100;
this.calculateElapsed();
this.calculateRemaining();
this.cd.markForCheck();
}, 100);
}
stopSeeker() {
clearInterval(this.seek_interval);
}
enableKeyControls() {
window.removeEventListener('keydown', this.keyPressListener, true);
window.addEventListener('keydown', this.keyPressListener, true);
}
disableKeyControls() {
window.removeEventListener('keydown', this.keyPressListener, true);
}
togglePause() {
if (this.element.paused === false) {
this.element.pause();
} else {
this.element.play();
}
}
moveToTime(offset) {
this.element.currentTime = this.element.currentTime + offset;
}
executeControl(e) {
e.preventDefault();
switch (e.keyCode) {
case 39:
this.moveToTime(2);
break;
case 37:
this.moveToTime(-2);
break;
case 32:
this.togglePause();
break;
}
}
}
<div
*ngIf="qualities?.length > 1"
class="m-video--quality-control-wrapper"
title="Video quality"
i18n-title="@@VIDEO__QUALITY_SELECTOR__VIDEO_QUALITY_TOOLTIP"
>
<i class="material-icons">high_quality</i>
<ul class="m-video--quality-control">
<li
*ngFor="let quality of qualities"
(click)="selectQuality(quality)"
[ngClass]="{ 'm-video--selected-quality': current === quality }"
>
{{ quality }}
</li>
</ul>
</div>
.m-video--quality-control-wrapper {
position: relative;
}
.m-video--quality-control:hover {
display: block;
}
.m-video--quality-control-wrapper:hover .m-video--quality-control {
display: block;
}
.m-video--quality-control {
display: none;
bottom: 24px;
left: 0px;
z-index: 1;
position: absolute;
margin: 0;
list-style: none;
padding: 8px;
cursor: pointer;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.4);
}
}
.m-video--selected-quality {
font-weight: bold;
}
///<reference path="../../../../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
Component,
DebugElement,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule as NgCommonModule } from '@angular/common';
import { MindsVideoQualitySelector } from './quality-selector.component';
describe('MindsVideoQualitySelector', () => {
let comp: MindsVideoQualitySelector;
let fixture: ComponentFixture<MindsVideoQualitySelector>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MindsVideoQualitySelector], // declare the test component
imports: [FormsModule, RouterTestingModule, NgCommonModule],
}).compileComponents(); // compile template and css
}));
beforeEach(done => {
window.addEventListener = () => {};
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(MindsVideoQualitySelector);
comp = fixture.componentInstance;
comp.qualities = ['720', '360', '128'];
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should render a hidden slider, should show as many options as there srcs, and first one should be selected', () => {
comp.current = '720';
fixture.detectChanges();
const wrapper = fixture.debugElement.query(
By.css('.m-video--quality-control-wrapper')
);
const control = fixture.debugElement.query(
By.css('.m-video--quality-control')
);
const icon = fixture.debugElement.query(By.css('.material-icons'));
const selectedOption = fixture.debugElement.query(
By.css('.m-video--selected-quality')
);
expect(control).not.toBeNull();
expect(icon).not.toBeNull();
expect(wrapper).not.toBeNull();
expect(selectedOption).not.toBeNull();
expect(selectedOption.nativeElement.innerText).toBe('720');
});
it('should change quality', () => {
comp.current = '720';
fixture.detectChanges();
const selectedOptions = fixture.debugElement.queryAll(By.css('li'));
selectedOptions[1].nativeElement.click();
fixture.detectChanges();
const selectedOption = fixture.debugElement.query(
By.css('.m-video--selected-quality')
);
expect(selectedOption).not.toBeNull();
expect(selectedOption.nativeElement.innerText).toBe('360');
});
});
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'm-video--quality-selector',
templateUrl: 'quality-selector.component.html',
})
export class MindsVideoQualitySelector {
@Input() current: string;
@Output('select') selectEmitter: EventEmitter<string> = new EventEmitter();
qualities: string[] = [];
@Input('qualities') set _qualities(qualities) {
if (!qualities || !qualities.length) {
this.qualities = [];
return;
}
this.qualities = qualities
.map(quality => quality)
.sort((a, b) => parseFloat(b) - parseFloat(a));
}
selectQuality(quality) {
this.current = quality;
this.selectEmitter.emit(quality);
}
}
export const SOURCE_CANDIDATE_PICK_LINEAR = 1;
export const SOURCE_CANDIDATE_PICK_ZIGZAG = 2;
export class SourceCandidates {
protected candidates: { [index: string]: any[] } = {};
protected blacklist = [];
protected lastBlacklistedType;
setSource(type: string, values: any[]) {
this.candidates[type] = values;
}
markAsClean() {
this.blacklist = [];
this.lastBlacklistedType = void 0;
}
setAsBlacklisted(type: string, value: any) {
this.blacklist.push({ type, value });
this.lastBlacklistedType = type;
// console.log('[sourcecandidates] blacklisted', { type, value }, JSON.stringify(this), this);
}
isBlacklisted(type: string, value: any) {
return (
this.blacklist.findIndex(
item => item.type === type && item.value === value
) > -1
);
}
empty() {
this.candidates = {};
this.markAsClean();
}
pick(
typePriorities: string[],
strategy: number = SOURCE_CANDIDATE_PICK_LINEAR
): { type; value } {
switch (strategy) {
case SOURCE_CANDIDATE_PICK_ZIGZAG:
return this._pickZigZag(typePriorities);
case SOURCE_CANDIDATE_PICK_LINEAR:
default:
return this._pickLinear(typePriorities);
}
}
private _pickZigZag(typePriorities: string[]): { type; value } {
const reorderedTypePriorities = typePriorities;
if (this.lastBlacklistedType) {
const index: number = reorderedTypePriorities.findIndex(
type => type === this.lastBlacklistedType
);
if (index > -1) {
reorderedTypePriorities.push(
...reorderedTypePriorities.splice(index, 1)
);
}
}
return this._pickLinear(reorderedTypePriorities);
}
private _pickLinear(typePriorities: string[]): { type; value } {
for (let type of typePriorities) {
if (!this.candidates[type]) {
continue;
}
const candidates = this.candidates[type].filter(
value => !this.isBlacklisted(type, value)
);
if (candidates.length > 0) {
return {
type,
value: candidates[0],
};
}
}
return void 0;
}
}
<ng-container *ngIf="current">
<m-video--direct-http-player
*ngIf="current.type === 'direct-http' || true"
class="m-video--player"
[src]="current.src"
[poster]="poster"
[muted]="muted"
[autoplay]="autoplay"
[guid]="guid"
(onPlay)="onPlay()"
(onPause)="onPause()"
(onEnd)="onEnd()"
(onError)="onError()"
(onCanPlayThrough)="onCanPlayThrough()"
(onLoadedMetadata)="loadedMetadata()"
(click)="clickedVideo()"
#player
></m-video--direct-http-player>
<m-video--torrent-player
*ngIf="current.type === 'torrent' && false"
class="m-video--player"
[src]="current.src"
[poster]="poster"
[muted]="muted"
[autoplay]="autoplay"
[guid]="guid"
(onPlay)="onPlay()"
(onPause)="onPause()"
(onEnd)="onEnd()"
(onError)="onError()"
(onCanPlayThrough)="onCanPlayThrough()"
(onLoadedMetadata)="loadedMetadata()"
(click)="clickedVideo()"
#player
></m-video--torrent-player>
<ng-container *ngIf="playerRef">
<i
*ngIf="
(!playerRef.isPlaying() && !playerRef.isLoading()) ||
(shouldPlayInModal && !playerRef.isPlaying())
"
class="material-icons minds-video-play-icon"
(click)="clickedVideo()"
>play_circle_outline</i
>
<ng-content></ng-content>
<div *ngIf="transcoding" class="minds-video-bar-top">
<span i18n="@@MEDIA__VIDEO__TRANSCODING_NOTICE"
>The video is being transcoded</span
>
</div>
<div *ngIf="transcodingError" class="minds-video-bar-top">
<span i18n="@@MEDIA__VIDEO__TRANSCODING_NOTICE">{{
transcodingError
}}</span>
</div>
<div
class="minds-video-bar-full"
[@fadeAnimation]="showControls ? 'in' : 'out'"
>
<i class="material-icons" (click)="controlBarToggle($event)">{{
playerRef.isPlaying() || playerRef.isLoading() ? 'pause' : 'play_arrow'
}}</i>
<ng-template #loadingSpinner>
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</ng-template>
<m-video--progress-bar
#progressBar
[player]="playerRef"
></m-video--progress-bar>
<m-video--volume-slider
#volumeSlider
[player]="playerRef"
></m-video--volume-slider>
<a
class="material-icons m-video-full-page minds-video--open-new"
*ngIf="guid && !isModal"
[routerLink]="['/media', guid]"
target="_blank"
(click)="playerRef.pause()"
>
lightbulb_outline
</a>
<ng-container *ngIf="current.type === 'torrent'">
<a
class="mdl-color-text--white m-video--info-button"
(click)="toggleTorrentInfo()"
>
<m-tooltip
icon="people_outline"
anchor="bottom"
i18n="@@MEDIA__VIDEO__PEERS_LABEL"
>Peers</m-tooltip
>
<span>{{ playerRef.getInfo().peers | abbr }}</span>
</a>
<a
class="mdl-color-text--white m-video--info-button"
(click)="toggleTorrentInfo()"
>
<m-tooltip
icon="arrow_downward"
anchor="bottom"
i18n="@@MEDIA__VIDEO__DOWNLOADING_LABEL"
>Downloading</m-tooltip
>
<span>{{ playerRef.getInfo().dlspeed | abbr: 2:true }}B/s</span>
</a>
<a
class="mdl-color-text--white m-video--info-button"
(click)="toggleTorrentInfo()"
>
<m-tooltip
icon="arrow_upward"
anchor="bottom"
i18n="@@MEDIA__VIDEO__UPLOADING_LABEL"
>Uploading</m-tooltip
>
<span>{{ playerRef.getInfo().ulspeed | abbr: 2:true }}B/s</span>
</a>
</ng-container>
<m-video--quality-selector
*ngIf="availableQualities?.length > 1"
[current]="currentQuality"
[qualities]="availableQualities"
(select)="selectedQuality($event)"
></m-video--quality-selector>
<i
*ngIf="!isModal && !isActivity"
class="material-icons"
(click)="toggleFullscreen($event)"
>tv</i
>
</div>
<div
class="m-video--torrent-info"
*ngIf="torrentInfo && current.type === 'torrent'"
>
<div class="m-video--torrent-info--cell">
<i class="material-icons">file_download</i>
<span>{{ playerRef.getInfo().progress * 100 | number: '1.2-2' }}%</span>
</div>
<div class="m-video--torrent-info--cell">
<i class="material-icons">people</i>
<span>{{ playerRef.getInfo().peers | number }}</span>
</div>
<div class="m-video--torrent-info--cell">
<i class="material-icons">arrow_downward</i>
<span
>{{ playerRef.getInfo().dl | abbr: 2:true }}B ({{
playerRef.getInfo().dlspeed | abbr: 2:true
}}B/s)</span
>
</div>
<div class="m-video--torrent-info--cell">
<i class="material-icons">arrow_upward</i>
<span
>{{ playerRef.getInfo().ul | abbr: 2:true }}B ({{
playerRef.getInfo().ulspeed | abbr: 2:true
}}B/s)</span
>
</div>
</div>
</ng-container>
</ng-container>
/**
* Minds video components
*/
m-video {
position: relative;
display: block;
&.clickable {
cursor: pointer;
}
&:hover {
.minds-video-play-icon {
opacity: 1;
}
}
video {
width: 100%;
}
.m-video--player {
display: block;
}
.minds-video-bar-min {
position: absolute;
bottom: 8px;
left: 8px;
width: auto;
padding: 4px 8px;
font-size: 11px;
border-radius: 3px;
font-weight: bold;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.4);
color: themed($m-white-always);
}
}
.minds-video-bar-top {
position: absolute;
top: 0;
left: 0;
width: 100%;
box-sizing: border-box;
padding: $minds-padding;
text-align: center;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.4);
color: themed($m-white-always);
}
}
.minds-video-play-icon {
opacity: 0.8;
display: block;
text-align: center;
top: 50%;
transform: translateY(-50%);
font-size: 100px;
position: absolute;
cursor: pointer;
width: 100%;
transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
@include m-theme() {
color: themed($m-white-always);
text-shadow: 0 0 3px rgba(themed($m-black-always), 0.6);
}
}
.minds-video-bar-full {
opacity: 0;
visibility: hidden;
display: flex;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
text-align: center;
align-items: center;
@include m-theme() {
color: themed($m-white-always);
background-color: rgba(themed($m-black-always), 0.4);
}
.m-video-full-page {
@include m-theme() {
color: themed($m-white-always);
}
}
.mdl-spinner {
margin: 0 8px;
}
> m-video--quality-selector,
> a,
> i {
cursor: pointer;
text-decoration: none;
padding: 12px;
}
}
&:hover {
.minds-video-bar-min {
display: none;
}
// .minds-video-bar-full{
// display: flex;
// }
}
.m-video--torrent-info {
position: absolute;
top: 8px;
left: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 8px;
font-size: 12px;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.65);
color: rgba(themed($m-white-always), 0.65);
}
}
.m-video--torrent-info--cell {
display: flex;
align-items: center;
margin: 4px 0;
&:last-child {
margin-right: 0;
}
.material-icons {
font-size: 16px;
line-height: 1;
margin-right: 4px;
@include m-theme() {
color: rgba(themed($m-white-always), 0.85);
}
}
}
.m-video--info-button {
display: flex;
align-items: center;
line-height: 1;
margin-right: 4px;
padding: 12px 4px !important;
> .material-icons {
margin-right: 4px;
}
> span {
font-size: 12px;
font-weight: 300;
}
}
}
......@@ -4,15 +4,8 @@ import { RouterModule } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '../../../../common/common.module';
import { MindsVideoProgressBar } from './progress-bar/progress-bar.component';
import { MindsVideoQualitySelector } from './quality-selector/quality-selector.component';
import { MindsVideoVolumeSlider } from './volume-slider/volume-slider.component';
import { VideoAdsDirective } from './ads.directive';
import { VideoAds, MindsVideoComponent } from './video.component';
import { MindsVideoDirectHttpPlayer } from './players/direct-http.component';
import { MindsVideoTorrentPlayer } from './players/torrent.component';
import { MindsVideoPlayerComponent } from '../video-player/player.component';
import { PlyrModule } from 'ngx-plyr';
@NgModule({
imports: [
......@@ -20,17 +13,9 @@ import { MindsVideoTorrentPlayer } from './players/torrent.component';
CommonModule,
FormsModule,
RouterModule.forChild([]),
PlyrModule,
],
declarations: [
VideoAdsDirective,
VideoAds,
MindsVideoComponent,
MindsVideoDirectHttpPlayer,
MindsVideoTorrentPlayer,
MindsVideoProgressBar,
MindsVideoQualitySelector,
MindsVideoVolumeSlider,
],
exports: [VideoAdsDirective, VideoAds, MindsVideoComponent],
declarations: [MindsVideoPlayerComponent],
exports: [MindsVideoPlayerComponent],
})
export class VideoModule {}
<div class="m-video--volume-control-wrapper" *ngIf="element">
<i
class="material-icons"
*ngIf="element.volume === 0 || element.muted"
(click)="element.muted = false"
>volume_off</i
>
<i
class="material-icons"
*ngIf="element.volume > 0.01 && element.volume <= 0.1 && !element.muted"
(click)="element.muted = true"
>volume_mute</i
>
<i
class="material-icons"
*ngIf="element.volume > 0.1 && element.volume < 0.9 && !element.muted"
(click)="element.muted = true"
>volume_down</i
>
<i
class="material-icons"
*ngIf="element.volume >= 0.9 && !element.muted"
(click)="element.muted = true"
>volume_up</i
>
<div class="m-video--volume-control">
<div class="m-video--volume-control--background"></div>
<input
type="range"
[(ngModel)]="element.volume"
class="m-video--volume-control-selector"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
m-video--volume-slider {
outline: none;
cursor: pointer;
text-decoration: none;
.m-video--volume-control-wrapper {
position: relative;
display: block;
i {
padding: 12px;
}
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
outline: none;
height: 16px;
width: 16px;
border-radius: 16px;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white-always);
}
}
input[type='range']::-moz-range-thumb {
height: 16px;
width: 16px;
outline: none;
border-radius: 16px;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white-always);
}
}
input[type='range']::-ms-thumb {
height: 16px;
outline: none;
width: 16px;
border-radius: 16px;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white-always);
}
}
.m-video--volume-control-selector {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none; /* WebKit */
-webkit-transform: rotate(270deg);
-moz-transform: rotate(270deg);
-o-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
outline: none;
height: 2px;
width: 70px;
position: absolute;
top: 35px;
right: -18px;
@include m-theme() {
background-color: themed($m-white-always);
}
}
.m-video--volume-control {
display: none;
bottom: 32px;
left: calc(50% - 20px);
z-index: 1;
width: 40px;
height: 80px;
position: absolute;
margin: 0;
}
.m-video--volume-control--background {
width: 40px;
height: 72px;
position: absolute;
left: calc(50% - 20px);
bottom: 16px;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.4);
}
}
.m-video--volume-control:hover {
display: block;
}
.m-video--volume-control-wrapper:hover .m-video--volume-control {
display: block;
}
}
///<reference path="../../../../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import {
Component,
DebugElement,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule as NgCommonModule } from '@angular/common';
import { MindsVideoVolumeSlider } from './volume-slider.component';
import { MindsPlayerInterface } from '../players/player.interface';
class MindsVideoDirectHttpPlayerMock implements MindsPlayerInterface {
@Input() muted: boolean;
@Input() poster: string;
@Input() autoplay: boolean;
@Input() src: string;
@Output() onPlay: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onPause: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onEnd: EventEmitter<HTMLVideoElement> = new EventEmitter();
@Output() onError: EventEmitter<{
player: HTMLVideoElement;
e;
}> = new EventEmitter();
getPlayer = (): HTMLVideoElement => {
return null;
};
play = () => {};
pause = () => {};
toggle = () => {};
resumeFromTime = () => {};
isLoading = (): boolean => {
return false;
};
isPlaying = (): boolean => {
return false;
};
requestFullScreen = jasmine.createSpy('requestFullScreen').and.stub();
getInfo = () => {};
}
describe('MindsVideoVolumeSlider', () => {
let comp: MindsVideoVolumeSlider;
let fixture: ComponentFixture<MindsVideoVolumeSlider>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MindsVideoVolumeSlider], // declare the test component
imports: [FormsModule, RouterTestingModule, NgCommonModule],
}).compileComponents(); // compile template and css
}));
beforeEach(done => {
window.addEventListener = () => {};
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(MindsVideoVolumeSlider);
comp = fixture.componentInstance;
const video = document.createElement('video');
video.src = 'thisisavideo.mp4';
comp.element = video;
const playerRef = new MindsVideoDirectHttpPlayerMock();
playerRef.getPlayer = () => {
return video;
};
comp.playerRef = playerRef;
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should render a hidden slider', () => {
const wrapper = fixture.debugElement.query(
By.css('.m-video--volume-control-wrapper')
);
const control = fixture.debugElement.query(
By.css('.m-video--volume-control')
);
const icon = fixture.debugElement.query(By.css('.material-icons'));
const input = fixture.debugElement.query(By.css('input'));
expect(control).not.toBeNull();
expect(input).not.toBeNull();
expect(icon).not.toBeNull();
expect(wrapper).not.toBeNull();
});
});
import { Component, Input } from '@angular/core';
import { MindsPlayerInterface } from '../players/player.interface';
@Component({
selector: 'm-video--volume-slider',
templateUrl: 'volume-slider.component.html',
})
export class MindsVideoVolumeSlider {
@Input('player') playerRef: MindsPlayerInterface;
element: HTMLVideoElement;
ngOnInit() {
this.bindToElement();
}
ngAfterViewInit() {
this.bindToElement();
}
bindToElement() {
if (this.playerRef.getPlayer()) {
this.element = this.playerRef.getPlayer();
}
}
}
......@@ -48,24 +48,13 @@
[style.width]="mediaWidth + 'px'"
[style.height]="mediaHeight + 'px'"
>
<m-video
class="m-mediaModal__media--video"
[style.height]="entityHeight + 'px'"
[style.width]="entityWidth + 'px'"
[isModal]="true"
[autoplay]="true"
[muted]="false"
[poster]="entity.thumbnail_src"
[src]="videoDirectSrc"
[guid]="entity.entity_guid"
[playCount]="entity['play:count']"
[torrent]="videoTorrentSrc"
(videoCanPlayThrough)="isLoaded()"
[@slowFadeAnimation]="isLoading ? 'out' : 'in'"
#player
>
<video-ads [player]="player" *ngIf="entity.monetized"></video-ads>
</m-video>
<m-videoPlayer
[guid]="entity.guid"
shouldPlayInModal="false"
isModal="true"
autoplay="true"
(fullScreenChange)="onFullscreenChange($event)"
></m-videoPlayer>
</div>
<!-- RICH-EMBED -->
......
......@@ -168,47 +168,13 @@ m-overlay-modal {
vertical-align: middle;
.m-mediaModal__media--image,
m-video {
m-videoPlayer {
display: inline-block;
max-height: 100%;
max-width: 100%;
vertical-align: top;
}
m-video {
position: static;
video {
width: 100%;
height: 100%;
}
.minds-video-bar-full {
.m-video--progress-bar {
padding-right: 0;
}
.m-video--volume-control-wrapper {
margin-right: 16px;
}
}
.minds-video-play-icon {
transform: none;
width: auto;
top: unquote('-webkit-calc(50% - 50px)');
left: unquote('-webkit-calc(50% - 50px)');
top: unquote('-moz-calc(50% - 50px)');
left: unquote('-moz-calc(50% - 50px)');
top: unquote('calc(50% - 50px)');
left: unquote('calc(50% - 50px)');
}
m-video--progress-bar {
.progress-bar {
margin-right: 8px;
}
}
}
&.m-mediaModal__mediaWrapper--blog {
overflow-x: hidden;
overflow-y: scroll;
......
......@@ -21,7 +21,6 @@ import { Subscription } from 'rxjs';
import { Session } from '../../../services/session';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { AnalyticsService } from '../../../services/analytics';
import { MindsVideoComponent } from '../components/video/video.component';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import { ActivityService } from '../../../common/services/activity.service';
import { SiteService } from '../../../common/services/site.service';
......@@ -116,10 +115,6 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.redirectUrl = params.redirectUrl || null;
}
// Used to make sure video progress bar seeker / hover works
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
videoDirectSrc = [];
videoTorrentSrc = [];
......@@ -594,21 +589,10 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// Show overlay and video controls
onMouseEnterStage() {
this.overlayVisible = true;
if (this.contentType === 'video') {
// Make sure progress bar seeker is updating when video controls are visible
this.videoComponent.stageHover = true;
this.videoComponent.onMouseEnter();
}
}
onMouseLeaveStage() {
this.overlayVisible = false;
if (this.contentType === 'video') {
// Stop updating progress bar seeker when controls aren't visible
this.videoComponent.stageHover = false;
this.videoComponent.onMouseLeave();
}
}
// * TABLETS ONLY: SHOW OVERLAY & VIDEO CONTROLS * -------------------------------------------
......
......@@ -6,7 +6,6 @@ import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { RecommendedService } from '../../components/video/recommended.service';
import { MindsVideoComponent } from '../../components/video/video.component';
@Component({
selector: 'm-media--theatre',
......@@ -38,19 +37,11 @@ import { MindsVideoComponent } from '../../components/video/video.component';
>.
</span>
</div>
<m-video
[poster]="object.thumbnail_src"
[autoplay]="!object.monetized"
[muted]="false"
(finished)="loadNext()"
[src]="videoDirectSrc"
[torrent]="videoTorrentSrc"
[log]="object.guid"
[playCount]="false"
#player
>
<video-ads [player]="player" *ngIf="object.monetized"></video-ads>
</m-video>
<m-videoPlayer
[guid]="object.guid"
[shouldPlayInModal]="false"
[autoplay]="true"
></m-videoPlayer>
</div>
<i class="material-icons right" (click)="next()" [hidden]="!isAlbum()">
keyboard_arrow_right
......@@ -68,9 +59,6 @@ export class MediaTheatreComponent {
minds = window.Minds;
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
videoDirectSrc = [];
videoTorrentSrc = [];
......@@ -81,43 +69,9 @@ export class MediaTheatreComponent {
private recommended: RecommendedService
) {}
updateSources() {
this.videoDirectSrc = [
{
res: '720',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=720',
type: 'video/mp4',
},
{
res: '360',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal',
type: 'video/mp4',
},
];
this.videoTorrentSrc = [
{ res: '720', key: this.object.guid + '/720.mp4' },
{ res: '360', key: this.object.guid + '/360.mp4' },
];
if (this.object.flags.full_hd) {
this.videoDirectSrc.unshift({
res: '1080',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=1080',
type: 'video/mp4',
});
this.videoTorrentSrc.unshift({
res: '1080',
key: this.object.guid + '/1080.mp4',
});
}
}
set _object(value: any) {
if (!value.guid) return;
this.object = value;
this.updateSources();
}
getThumbnail() {
......@@ -178,22 +132,6 @@ export class MediaTheatreComponent {
this.timerSubscribe.unsubscribe();
}
togglePlay($event) {
this.videoComponent.toggle();
}
// Show video controls
onMouseEnterStage() {
this.videoComponent.stageHover = true;
this.videoComponent.onMouseEnter();
}
// Hide video controls
onMouseLeaveStage() {
this.videoComponent.stageHover = false;
this.videoComponent.onMouseLeave();
}
ngOnDestroy() {
if (this.timerSubscribe) {
this.timerSubscribe.unsubscribe();
......
......@@ -1778,6 +1778,23 @@
</a>
</ng-template>
<ng-template ngSwitchCase="transcode_completed">
<a [routerLink]="['/media/', notification.entity.guid]">
<p i18n="@@NOTIFICATIONS__NOTIFICATION__REWARDS__YOU_HAVE_LEFT">
Your video has finished transcoding.
</p>
</a>
</ng-template>
<ng-template ngSwitchCase="transcode_failed">
<a [routerLink]="['/media/', notification.entity.guid]">
<p i18n="@@NOTIFICATIONS__NOTIFICATION__REWARDS__YOU_HAVE_LEFT">
Your video failed to transcode. Please check that your format is valid
and try to upload the video again.
</p>
</a>
</ng-template>
<!-- Default -->
<ng-template ngSwitchDefault>
<i i18n="@@NOTIFICATIONS__NOTIFICATION__GENERAL_ERROR_VIEWING"
......
......@@ -3,26 +3,13 @@
(click)="tileClicked()"
[ngSwitch]="getType(entity)"
>
<m-video
<m-videoPlayer
*ngSwitchCase="'object:video'"
width="100%"
height="300px"
[muted]="false"
[poster]="entity.thumbnail_src"
[src]="[
{
res: '360',
uri: 'api/v1/media/' + entity.guid + '/play?s=tile',
type: 'video/mp4'
}
]"
[guid]="entity.guid"
[playCount]="entity['play:count']"
[torrent]="[{ res: '360', key: entity.guid + '/360.mp4' }]"
(videoMetadataLoaded)="setVideoDimensions($event)"
[shouldPlayInModal]="true"
#player
></m-video>
[autoplay]="false"
(mediaModalRequested)="openModal()"
></m-videoPlayer>
<img *ngSwitchDefault [src]="entity.thumbnail_src" #img />
</div>
......
Please register or to comment