aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts13
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts6
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts13
-rw-r--r--client/src/app/app.component.ts4
-rw-r--r--client/src/app/core/hotkeys/hotkeys.component.scss9
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.html2
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.ts13
-rw-r--r--client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss14
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.html2
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts8
-rw-r--r--client/src/app/shared/video/video-details.model.ts4
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss11
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.html25
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.scss59
-rw-r--r--client/src/app/videos/+video-watch/video-watch-playlist.component.ts113
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html34
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss66
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts116
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts2
20 files changed, 287 insertions, 229 deletions
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 1814ef455..0d579fa0c 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -7,7 +7,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
7import { VideoService } from '../../shared/video/video.service' 7import { VideoService } from '../../shared/video/video.service'
8import { Account } from '@app/shared/account/account.model' 8import { Account } from '@app/shared/account/account.model'
9import { AccountService } from '@app/shared/account/account.service' 9import { AccountService } from '@app/shared/account/account.service'
10import { tap } from 'rxjs/operators' 10import { first, tap } from 'rxjs/operators'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { Subscription } from 'rxjs' 12import { Subscription } from 'rxjs'
13import { ScreenService } from '@app/shared/misc/screen.service' 13import { ScreenService } from '@app/shared/misc/screen.service'
@@ -50,12 +50,13 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
50 50
51 // Parent get the account for us 51 // Parent get the account for us
52 this.accountSub = this.accountService.accountLoaded 52 this.accountSub = this.accountService.accountLoaded
53 .subscribe(account => { 53 .pipe(first())
54 this.account = account 54 .subscribe(account => {
55 this.account = account
55 56
56 this.reloadVideos() 57 this.reloadVideos()
57 this.generateSyndicationList() 58 this.generateSyndicationList()
58 }) 59 })
59 } 60 }
60 61
61 ngOnDestroy () { 62 ngOnDestroy () {
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
index f878a5a24..907aefae1 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
@@ -1,9 +1,7 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { AuthService } from '../../core/auth'
3import { ConfirmService } from '../../core/confirm' 2import { ConfirmService } from '../../core/confirm'
4import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 3import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 4import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { flatMap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 5import { Subscription } from 'rxjs'
8import { Notifier } from '@app/core' 6import { Notifier } from '@app/core'
9import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 7import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
@@ -28,7 +26,6 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
28 private videoChannel: VideoChannel 26 private videoChannel: VideoChannel
29 27
30 constructor ( 28 constructor (
31 private authService: AuthService,
32 private notifier: Notifier, 29 private notifier: Notifier,
33 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
34 private videoPlaylistService: VideoPlaylistService, 31 private videoPlaylistService: VideoPlaylistService,
@@ -57,8 +54,7 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
57 } 54 }
58 55
59 private loadVideoPlaylists () { 56 private loadVideoPlaylists () {
60 this.authService.userInformationLoaded 57 this.videoPlaylistService.listChannelPlaylists(this.videoChannel)
61 .pipe(flatMap(() => this.videoPlaylistService.listChannelPlaylists(this.videoChannel)))
62 .subscribe(res => { 58 .subscribe(res => {
63 this.videoPlaylists = this.videoPlaylists.concat(res.data) 59 this.videoPlaylists = this.videoPlaylists.concat(res.data)
64 this.pagination.totalItems = res.total 60 this.pagination.totalItems = res.total
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 2045a095d..5e60b34b4 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -7,7 +7,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
7import { VideoService } from '../../shared/video/video.service' 7import { VideoService } from '../../shared/video/video.service'
8import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 8import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
9import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 9import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
10import { tap } from 'rxjs/operators' 10import { first, tap } from 'rxjs/operators'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { Subscription } from 'rxjs' 12import { Subscription } from 'rxjs'
13import { ScreenService } from '@app/shared/misc/screen.service' 13import { ScreenService } from '@app/shared/misc/screen.service'
@@ -50,12 +50,13 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
50 50
51 // Parent get the video channel for us 51 // Parent get the video channel for us
52 this.videoChannelSub = this.videoChannelService.videoChannelLoaded 52 this.videoChannelSub = this.videoChannelService.videoChannelLoaded
53 .subscribe(videoChannel => { 53 .pipe(first())
54 this.videoChannel = videoChannel 54 .subscribe(videoChannel => {
55 this.videoChannel = videoChannel
55 56
56 this.reloadVideos() 57 this.reloadVideos()
57 this.generateSyndicationList() 58 this.generateSyndicationList()
58 }) 59 })
59 } 60 }
60 61
61 ngOnDestroy () { 62 ngOnDestroy () {
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index ad0588b99..915466af7 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -125,8 +125,8 @@ export class AppComponent implements OnInit {
125 try { 125 try {
126 resetScroll = false 126 resetScroll = false
127 127
128 const previousUrl = new URL(window.location.origin + e1.url) 128 const previousUrl = new URL(window.location.origin + e1.urlAfterRedirects)
129 const nextUrl = new URL(window.location.origin + e2.url) 129 const nextUrl = new URL(window.location.origin + e2.urlAfterRedirects)
130 130
131 if (previousUrl.pathname !== nextUrl.pathname) { 131 if (previousUrl.pathname !== nextUrl.pathname) {
132 resetScroll = true 132 resetScroll = true
diff --git a/client/src/app/core/hotkeys/hotkeys.component.scss b/client/src/app/core/hotkeys/hotkeys.component.scss
index 9af10b7c4..3aa0b6252 100644
--- a/client/src/app/core/hotkeys/hotkeys.component.scss
+++ b/client/src/app/core/hotkeys/hotkeys.component.scss
@@ -1,5 +1,6 @@
1.cfp-hotkeys-container { 1.cfp-hotkeys-container {
2 display: table !important; 2 display: flex !important;
3 align-items: center;
3 position: fixed; 4 position: fixed;
4 overflow: auto; 5 overflow: auto;
5 width: 100%; 6 width: 100%;
@@ -35,9 +36,7 @@
35 36
36.cfp-hotkeys { 37.cfp-hotkeys {
37 width: 100%; 38 width: 100%;
38 height: 100%; 39 max-height: 100%;
39 display: table-cell;
40 vertical-align: middle;
41} 40}
42 41
43.cfp-hotkeys table { 42.cfp-hotkeys table {
@@ -102,4 +101,4 @@
102 .cfp-hotkeys { 101 .cfp-hotkeys {
103 font-size: 1.2em; 102 font-size: 1.2em;
104 } 103 }
105} \ No newline at end of file 104}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.html b/client/src/app/shared/menu/top-menu-dropdown.component.html
index 54a8f9e80..35511ee62 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.html
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.html
@@ -3,7 +3,7 @@
3 3
4 <a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a> 4 <a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a>
5 5
6 <div *ngIf="!menuEntry.routerLink" ngbDropdown container="body" class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)"> 6 <div *ngIf="!menuEntry.routerLink" ngbDropdown [container]="container" class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)">
7 <span 7 <span
8 (mouseenter)="openDropdownOnHover(dropdown)" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor 8 (mouseenter)="openDropdownOnHover(dropdown)" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor
9 (click)="dropdownAnchorClicked(dropdown)" role="button" class="title-page" 9 (click)="dropdownAnchorClicked(dropdown)" role="button" class="title-page"
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
index e951ea236..5ccdafb54 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.ts
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -4,6 +4,7 @@ import { NavigationEnd, Router } from '@angular/router'
4import { Subscription } from 'rxjs' 4import { Subscription } from 'rxjs'
5import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' 5import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
6import { GlobalIconName } from '@app/shared/images/global-icon.component' 6import { GlobalIconName } from '@app/shared/images/global-icon.component'
7import { ScreenService } from '@app/shared/misc/screen.service'
7 8
8export type TopMenuDropdownParam = { 9export type TopMenuDropdownParam = {
9 label: string 10 label: string
@@ -27,11 +28,15 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
27 28
28 suffixLabels: { [ parentLabel: string ]: string } 29 suffixLabels: { [ parentLabel: string ]: string }
29 hasIcons = false 30 hasIcons = false
31 container: undefined | 'body' = undefined
30 32
31 private openedOnHover = false 33 private openedOnHover = false
32 private routeSub: Subscription 34 private routeSub: Subscription
33 35
34 constructor (private router: Router) {} 36 constructor (
37 private router: Router,
38 private screen: ScreenService
39 ) {}
35 40
36 ngOnInit () { 41 ngOnInit () {
37 this.updateChildLabels(window.location.pathname) 42 this.updateChildLabels(window.location.pathname)
@@ -43,6 +48,12 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
43 this.hasIcons = this.menuEntries.some( 48 this.hasIcons = this.menuEntries.some(
44 e => e.children && e.children.some(c => !!c.iconName) 49 e => e.children && e.children.some(c => !!c.iconName)
45 ) 50 )
51
52 // FIXME: We have to set body for the container to avoid because of scroll overflow on mobile view
53 // But this break our hovering system
54 if (this.screen.isInMobileView()) {
55 this.container = 'body'
56 }
46 } 57 }
47 58
48 ngOnDestroy () { 59 ngOnDestroy () {
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
index f8a068cbc..cb7072d7f 100644
--- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss
@@ -2,6 +2,13 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5my-video-thumbnail {
6 @include thumbnail-size-component(130px, 72px);
7
8 display: flex; // Avoids an issue with line-height that adds space below the element
9 margin-right: 10px;
10}
11
5.video { 12.video {
6 display: flex; 13 display: flex;
7 align-items: center; 14 align-items: center;
@@ -44,13 +51,6 @@
44 } 51 }
45 } 52 }
46 53
47 my-video-thumbnail {
48 @include thumbnail-size-component(130px, 72px);
49
50 display: flex; // Avoids an issue with line-height that adds space below the element
51 margin-right: 10px;
52 }
53
54 .video-info { 54 .video-info {
55 display: flex; 55 display: flex;
56 flex-direction: column; 56 flex-direction: column;
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.html b/client/src/app/shared/video/video-actions-dropdown.component.html
index 300fe318a..ec03fa55d 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.html
+++ b/client/src/app/shared/video/video-actions-dropdown.component.html
@@ -11,7 +11,7 @@
11 </div> 11 </div>
12 12
13 <my-action-dropdown 13 <my-action-dropdown
14 [actions]="videoActions" [label]="label" [entry]="{ video: video }" (mouseenter)="loadDropdownInformation()" 14 [actions]="videoActions" [label]="label" [entry]="{ video: video }" (click)="loadDropdownInformation()"
15 [buttonSize]="buttonSize" [placement]="placement" [buttonDirection]="buttonDirection" [buttonStyled]="buttonStyled" 15 [buttonSize]="buttonSize" [placement]="placement" [buttonDirection]="buttonDirection" [buttonStyled]="buttonStyled"
16 ></my-action-dropdown> 16 ></my-action-dropdown>
17 17
diff --git a/client/src/app/shared/video/video-actions-dropdown.component.ts b/client/src/app/shared/video/video-actions-dropdown.component.ts
index 787ef1188..ee2f44f9e 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -1,4 +1,4 @@
1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { DropdownAction, DropdownButtonSize, DropdownDirection } from '@app/shared/buttons/action-dropdown.component' 3import { DropdownAction, DropdownButtonSize, DropdownDirection } from '@app/shared/buttons/action-dropdown.component'
4import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' 4import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
@@ -126,6 +126,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
126 return this.video.isUnblacklistableBy(this.user) 126 return this.video.isUnblacklistableBy(this.user)
127 } 127 }
128 128
129 isVideoDownloadable () {
130 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
131 }
132
129 /* Action handlers */ 133 /* Action handlers */
130 134
131 async unblacklistVideo () { 135 async unblacklistVideo () {
@@ -195,7 +199,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
195 { 199 {
196 label: this.i18n('Download'), 200 label: this.i18n('Download'),
197 handler: () => this.showDownloadModal(), 201 handler: () => this.showDownloadModal(),
198 isDisplayed: () => this.displayOptions.download, 202 isDisplayed: () => this.displayOptions.download && this.isVideoDownloadable(),
199 iconName: 'download' 203 iconName: 'download'
200 }, 204 },
201 { 205 {
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 8463e15d7..22f024656 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -52,4 +52,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
52 getHlsPlaylist () { 52 getHlsPlaylist () {
53 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) 53 return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
54 } 54 }
55
56 hasHlsPlaylist () {
57 return !!this.getHlsPlaylist()
58 }
55} 59}
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss
index 6e173af99..d665ce021 100644
--- a/client/src/app/shared/video/video-miniature.component.scss
+++ b/client/src/app/shared/video/video-miniature.component.scss
@@ -2,6 +2,9 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5$more-button-width: 41px;
6$more-margin-right: 10px;
7
5.video-miniature { 8.video-miniature {
6 width: $video-miniature-width; 9 width: $video-miniature-width;
7 display: inline-flex; 10 display: inline-flex;
@@ -14,7 +17,7 @@
14 display: flex; 17 display: flex;
15 18
16 .video-miniature-information { 19 .video-miniature-information {
17 width: 200px; 20 width: $video-miniature-width - $more-button-width - $more-margin-right;
18 line-height: normal; 21 line-height: normal;
19 22
20 .video-miniature-name { 23 .video-miniature-name {
@@ -61,7 +64,9 @@
61 64
62 .video-actions { 65 .video-actions {
63 margin-top: 3px; 66 margin-top: 3px;
64 margin-right: 10px; 67 margin-right: $more-margin-right;
68 width: $more-button-width;
69 height: 30px;
65 70
66 /deep/ .dropdown-root:not(.show) { 71 /deep/ .dropdown-root:not(.show) {
67 opacity: 0; 72 opacity: 0;
@@ -86,7 +91,7 @@
86 top: -3px; 91 top: -3px;
87 92
88 /deep/ .dropdown-root { 93 /deep/ .dropdown-root {
89 display: block !important; 94 opacity: 1 !important;
90 } 95 }
91 } 96 }
92 } 97 }
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index e32b8cbc5..48475033c 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -74,7 +74,7 @@ export class VideoMiniatureComponent implements OnInit {
74 74
75 // We rely on mouseenter to lazy load actions 75 // We rely on mouseenter to lazy load actions
76 if (this.screenService.isInTouchScreen()) { 76 if (this.screenService.isInTouchScreen()) {
77 this.showActions = true 77 this.loadActions()
78 } 78 }
79 } 79 }
80 80
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.html b/client/src/app/videos/+video-watch/video-watch-playlist.component.html
new file mode 100644
index 000000000..c168a3130
--- /dev/null
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.html
@@ -0,0 +1,25 @@
1<div *ngIf="playlist && video" class="playlist" myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()">
2 <div class="playlist-info">
3 <div class="playlist-display-name">
4 {{ playlist.displayName }}
5
6 <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span>
7 <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span>
8 <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span>
9 </div>
10
11 <div class="playlist-by-index">
12 <div class="playlist-by">{{ playlist.ownerBy }}</div>
13 <div class="playlist-index">
14 <span>{{ currentPlaylistPosition }}</span><span>{{ playlistPagination.totalItems }}</span>
15 </div>
16 </div>
17 </div>
18
19 <div *ngFor="let playlistVideo of playlistVideos">
20 <my-video-playlist-element-miniature
21 [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
22 [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position"
23 ></my-video-playlist-element-miniature>
24 </div>
25</div>
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss
new file mode 100644
index 000000000..5da55c2f8
--- /dev/null
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss
@@ -0,0 +1,59 @@
1@import '_variables';
2@import '_mixins';
3@import '_bootstrap-variables';
4@import '_miniature';
5
6.playlist {
7 min-width: 200px;
8 max-width: 470px;
9 height: 66vh;
10 background-color: var(--mainBackgroundColor);
11 overflow-y: auto;
12 border-bottom: 1px solid $separator-border-color;
13
14 .playlist-info {
15 padding: 5px 30px;
16 background-color: #e4e4e4;
17
18 .playlist-display-name {
19 font-size: 18px;
20 font-weight: $font-semibold;
21 margin-bottom: 5px;
22 }
23
24 .playlist-by-index {
25 color: $grey-foreground-color;
26 display: flex;
27
28 .playlist-by {
29 margin-right: 5px;
30 }
31
32 .playlist-index span:first-child::after {
33 content: '/';
34 margin: 0 3px;
35 }
36 }
37 }
38
39 my-video-playlist-element-miniature {
40 /deep/ {
41 .video {
42 .position {
43 margin-right: 0;
44 }
45
46 .video-info {
47 .video-info-name {
48 font-size: 15px;
49 }
50 }
51 }
52
53 my-video-thumbnail {
54 @include thumbnail-size-component(90px, 50px);
55 }
56 }
57 }
58}
59
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
new file mode 100644
index 000000000..bccdaf7b2
--- /dev/null
+++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts
@@ -0,0 +1,113 @@
1import { Component, Input } from '@angular/core'
2import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
3import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
4import { Video } from '@app/shared/video/video.model'
5import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models'
6import { VideoService } from '@app/shared/video/video.service'
7import { Router } from '@angular/router'
8import { AuthService } from '@app/core'
9
10@Component({
11 selector: 'my-video-watch-playlist',
12 templateUrl: './video-watch-playlist.component.html',
13 styleUrls: [ './video-watch-playlist.component.scss' ]
14})
15export class VideoWatchPlaylistComponent {
16 @Input() video: VideoDetails
17 @Input() playlist: VideoPlaylist
18
19 playlistVideos: Video[] = []
20 playlistPagination: ComponentPagination = {
21 currentPage: 1,
22 itemsPerPage: 30,
23 totalItems: null
24 }
25
26 noPlaylistVideos = false
27 currentPlaylistPosition = 1
28
29 constructor (
30 private auth: AuthService,
31 private videoService: VideoService,
32 private router: Router
33 ) {}
34
35 onPlaylistVideosNearOfBottom () {
36 // Last page
37 if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return
38
39 this.playlistPagination.currentPage += 1
40 this.loadPlaylistElements(this.playlist,false)
41 }
42
43 onElementRemoved (video: Video) {
44 this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id)
45
46 this.playlistPagination.totalItems--
47 }
48
49 isPlaylistOwned () {
50 return this.playlist.isLocal === true &&
51 this.auth.isLoggedIn() &&
52 this.playlist.ownerAccount.name === this.auth.getUser().username
53 }
54
55 isUnlistedPlaylist () {
56 return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED
57 }
58
59 isPrivatePlaylist () {
60 return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
61 }
62
63 isPublicPlaylist () {
64 return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC
65 }
66
67 loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
68 this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination)
69 .subscribe(({ totalVideos, videos }) => {
70 this.playlistVideos = this.playlistVideos.concat(videos)
71 this.playlistPagination.totalItems = totalVideos
72
73 if (totalVideos === 0) {
74 this.noPlaylistVideos = true
75 return
76 }
77
78 this.updatePlaylistIndex(this.video)
79
80 if (redirectToFirst) {
81 const extras = {
82 queryParams: { videoId: this.playlistVideos[ 0 ].uuid },
83 replaceUrl: true
84 }
85 this.router.navigate([], extras)
86 }
87 })
88 }
89
90 updatePlaylistIndex (video: VideoDetails) {
91 if (this.playlistVideos.length === 0 || !video) return
92
93 for (const playlistVideo of this.playlistVideos) {
94 if (playlistVideo.id === video.id) {
95 this.currentPlaylistPosition = playlistVideo.playlistElement.position
96 return
97 }
98 }
99
100 // Load more videos to find our video
101 this.onPlaylistVideosNearOfBottom()
102 }
103
104 navigateToNextPlaylistVideo () {
105 if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
106 const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1)
107
108 const start = next.playlistElement.startTimestamp
109 const stop = next.playlistElement.stopTimestamp
110 this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } })
111 }
112 }
113}
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 7e9b89dd0..2e39b9c6b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -9,31 +9,10 @@
9 9
10 <div id="videojs-wrapper"></div> 10 <div id="videojs-wrapper"></div>
11 11
12 <div *ngIf="playlist && video" class="playlist" myInfiniteScroller [autoInit]="true" [onItself]="true" (nearOfBottom)="onPlaylistVideosNearOfBottom()"> 12 <my-video-watch-playlist
13 <div class="playlist-info"> 13 #videoWatchPlaylist
14 <div class="playlist-display-name"> 14 [video]="video" [playlist]="playlist" class="playlist"
15 {{ playlist.displayName }} 15 ></my-video-watch-playlist>
16
17 <span *ngIf="isUnlistedPlaylist()" class="badge badge-warning" i18n>Unlisted</span>
18 <span *ngIf="isPrivatePlaylist()" class="badge badge-danger" i18n>Private</span>
19 <span *ngIf="isPublicPlaylist()" class="badge badge-info" i18n>Public</span>
20 </div>
21
22 <div class="playlist-by-index">
23 <div class="playlist-by">{{ playlist.ownerBy }}</div>
24 <div class="playlist-index">
25 <span>{{ currentPlaylistPosition }}</span><span>{{ playlistPagination.totalItems }}</span>
26 </div>
27 </div>
28 </div>
29
30 <div *ngFor="let playlistVideo of playlistVideos">
31 <my-video-playlist-element-miniature
32 [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
33 [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position"
34 ></my-video-playlist-element-miniature>
35 </div>
36 </div>
37 </div> 16 </div>
38 17
39 <div class="row"> 18 <div class="row">
@@ -49,10 +28,6 @@
49 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. 28 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
50 </div> 29 </div>
51 30
52 <div i18n class="col-md-12 alert alert-info" *ngIf="noPlaylistVideos">
53 This playlist does not have videos.
54 </div>
55
56 <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted"> 31 <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted">
57 <div class="blacklisted-label" i18n>This video is blacklisted.</div> 32 <div class="blacklisted-label" i18n>This video is blacklisted.</div>
58 {{ video.blacklistedReason }} 33 {{ video.blacklistedReason }}
@@ -240,6 +215,7 @@
240 OK 215 OK
241 </div> 216 </div>
242 </div> 217 </div>
218</div>
243 219
244<ng-template [ngIf]="video !== null"> 220<ng-template [ngIf]="video !== null">
245 <my-video-support #videoSupportModal [video]="video"></my-video-support> 221 <my-video-support #videoSupportModal [video]="video"></my-video-support>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index d8113b666..8ca5c4118 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -15,10 +15,10 @@ $player-factor: 1.7; // 16/9
15} 15}
16 16
17@mixin playlist-below-player { 17@mixin playlist-below-player {
18 width: 100%; 18 width: 100% !important;
19 height: auto; 19 height: auto !important;
20 max-height: 300px; 20 max-height: 300px !important;
21 border-bottom: 1px solid $separator-border-color; 21 border-bottom: 1px solid $separator-border-color !important;
22} 22}
23 23
24.root { 24.root {
@@ -37,7 +37,7 @@ $player-factor: 1.7; // 16/9
37 width: 100%; 37 width: 100%;
38 } 38 }
39 39
40 .playlist { 40 my-video-watch-playlist /deep/ .playlist {
41 @include playlist-below-player; 41 @include playlist-below-player;
42 } 42 }
43 } 43 }
@@ -80,60 +80,6 @@ $player-factor: 1.7; // 16/9
80 } 80 }
81 } 81 }
82 82
83 .playlist {
84 min-width: 200px;
85 max-width: 470px;
86 height: 66vh;
87 background-color: var(--mainBackgroundColor);
88 overflow-y: auto;
89 border-bottom: 1px solid $separator-border-color;
90
91 .playlist-info {
92 padding: 5px 30px;
93 background-color: #e4e4e4;
94
95 .playlist-display-name {
96 font-size: 18px;
97 font-weight: $font-semibold;
98 margin-bottom: 5px;
99 }
100
101 .playlist-by-index {
102 color: $grey-foreground-color;
103 display: flex;
104
105 .playlist-by {
106 margin-right: 5px;
107 }
108
109 .playlist-index span:first-child::after {
110 content: '/';
111 margin: 0 3px;
112 }
113 }
114 }
115
116 my-video-playlist-element-miniature {
117 /deep/ {
118 .video {
119 .position {
120 margin-right: 0;
121 }
122
123 .video-info {
124 .video-info-name {
125 font-size: 15px;
126 }
127 }
128 }
129
130 my-video-thumbnail {
131 @include thumbnail-size-component(90px, 50px);
132 }
133 }
134 }
135 }
136
137 /deep/ .video-js { 83 /deep/ .video-js {
138 width: getPlayerWidth(66vh); 84 width: getPlayerWidth(66vh);
139 height: 66vh; 85 height: 66vh;
@@ -508,7 +454,7 @@ my-video-comments {
508 flex-direction: column; 454 flex-direction: column;
509 justify-content: center; 455 justify-content: center;
510 456
511 .playlist { 457 my-video-watch-playlist /deep/ .playlist {
512 @include playlist-below-player; 458 @include playlist-below-player;
513 } 459 }
514 } 460 }
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index bce652210..b147b75b0 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -8,7 +8,7 @@ import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
11import { UserVideoRateType, VideoCaption, VideoPlaylistPrivacy, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
12import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
13import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
14import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
@@ -27,9 +27,9 @@ import {
27} from '../../../assets/player/peertube-player-manager' 27} from '../../../assets/player/peertube-player-manager'
28import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' 28import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
29import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 29import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
30import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
31import { Video } from '@app/shared/video/video.model' 30import { Video } from '@app/shared/video/video.model'
32import { isWebRTCDisabled } from '../../../assets/player/utils' 31import { isWebRTCDisabled } from '../../../assets/player/utils'
32import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
33 33
34@Component({ 34@Component({
35 selector: 'my-video-watch', 35 selector: 'my-video-watch',
@@ -39,6 +39,7 @@ import { isWebRTCDisabled } from '../../../assets/player/utils'
39export class VideoWatchComponent implements OnInit, OnDestroy { 39export class VideoWatchComponent implements OnInit, OnDestroy {
40 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern' 40 private static LOCAL_STORAGE_PRIVACY_CONCERN_KEY = 'video-watch-privacy-concern'
41 41
42 @ViewChild('videoWatchPlaylist') videoWatchPlaylist: VideoWatchPlaylistComponent
42 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 43 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
43 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent 44 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent
44 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 45 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
@@ -51,14 +52,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
51 descriptionLoading = false 52 descriptionLoading = false
52 53
53 playlist: VideoPlaylist = null 54 playlist: VideoPlaylist = null
54 playlistVideos: Video[] = []
55 playlistPagination: ComponentPagination = {
56 currentPage: 1,
57 itemsPerPage: 30,
58 totalItems: null
59 }
60 noPlaylistVideos = false
61 currentPlaylistPosition = 1
62 55
63 completeDescriptionShown = false 56 completeDescriptionShown = false
64 completeVideoDescription: string 57 completeVideoDescription: string
@@ -230,10 +223,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
230 return this.video.tags 223 return this.video.tags
231 } 224 }
232 225
233 isVideoRemovable () {
234 return this.video.isRemovableBy(this.authService.getUser())
235 }
236
237 onVideoRemoved () { 226 onVideoRemoved () {
238 this.redirectService.redirectToHomepage() 227 this.redirectService.redirectToHomepage()
239 } 228 }
@@ -247,10 +236,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
247 return this.video && this.video.state.id === VideoState.TO_TRANSCODE 236 return this.video && this.video.state.id === VideoState.TO_TRANSCODE
248 } 237 }
249 238
250 isVideoDownloadable () {
251 return this.video && this.video.downloadEnabled
252 }
253
254 isVideoToImport () { 239 isVideoToImport () {
255 return this.video && this.video.state.id === VideoState.TO_IMPORT 240 return this.video && this.video.state.id === VideoState.TO_IMPORT
256 } 241 }
@@ -263,36 +248,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
263 return video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) 248 return video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
264 } 249 }
265 250
266 isPlaylistOwned () {
267 return this.playlist.isLocal === true && this.playlist.ownerAccount.name === this.user.username
268 }
269
270 isUnlistedPlaylist () {
271 return this.playlist.privacy.id === VideoPlaylistPrivacy.UNLISTED
272 }
273
274 isPrivatePlaylist () {
275 return this.playlist.privacy.id === VideoPlaylistPrivacy.PRIVATE
276 }
277
278 isPublicPlaylist () {
279 return this.playlist.privacy.id === VideoPlaylistPrivacy.PUBLIC
280 }
281
282 onPlaylistVideosNearOfBottom () {
283 // Last page
284 if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return
285
286 this.playlistPagination.currentPage += 1
287 this.loadPlaylistElements(false)
288 }
289
290 onElementRemoved (video: Video) {
291 this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id)
292
293 this.playlistPagination.totalItems--
294 }
295
296 private loadVideo (videoId: string) { 251 private loadVideo (videoId: string) {
297 // Video did not change 252 // Video did not change
298 if (this.video && this.video.uuid === videoId) return 253 if (this.video && this.video.uuid === videoId) return
@@ -333,33 +288,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
333 this.playlist = playlist 288 this.playlist = playlist
334 289
335 const videoId = this.route.snapshot.queryParams['videoId'] 290 const videoId = this.route.snapshot.queryParams['videoId']
336 this.loadPlaylistElements(!videoId) 291 this.videoWatchPlaylist.loadPlaylistElements(playlist, !videoId)
337 }) 292 })
338 } 293 }
339 294
340 private loadPlaylistElements (redirectToFirst = false) {
341 this.videoService.getPlaylistVideos(this.playlist.uuid, this.playlistPagination)
342 .subscribe(({ totalVideos, videos }) => {
343 this.playlistVideos = this.playlistVideos.concat(videos)
344 this.playlistPagination.totalItems = totalVideos
345
346 if (totalVideos === 0) {
347 this.noPlaylistVideos = true
348 return
349 }
350
351 this.updatePlaylistIndex()
352
353 if (redirectToFirst) {
354 const extras = {
355 queryParams: { videoId: this.playlistVideos[ 0 ].uuid },
356 replaceUrl: true
357 }
358 this.router.navigate([], extras)
359 }
360 })
361 }
362
363 private updateVideoDescription (description: string) { 295 private updateVideoDescription (description: string) {
364 this.video.description = description 296 this.video.description = description
365 this.setVideoDescriptionHTML() 297 this.setVideoDescriptionHTML()
@@ -421,7 +353,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
421 this.remoteServerDown = false 353 this.remoteServerDown = false
422 this.currentTime = undefined 354 this.currentTime = undefined
423 355
424 this.updatePlaylistIndex() 356 this.videoWatchPlaylist.updatePlaylistIndex(video)
425 357
426 let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) 358 let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
427 // If we are at the end of the video, reset the timer 359 // If we are at the end of the video, reset the timer
@@ -491,7 +423,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
491 } 423 }
492 } 424 }
493 425
494 const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' 426 let mode: PlayerMode
427
428 if (urlOptions.playerMode) {
429 if (urlOptions.playerMode === 'p2p-media-loader') mode = 'p2p-media-loader'
430 else mode = 'webtorrent'
431 } else {
432 if (this.video.hasHlsPlaylist()) mode = 'p2p-media-loader'
433 else mode = 'webtorrent'
434 }
495 435
496 if (mode === 'p2p-media-loader') { 436 if (mode === 'p2p-media-loader') {
497 const hlsPlaylist = this.video.getHlsPlaylist() 437 const hlsPlaylist = this.video.getHlsPlaylist()
@@ -519,13 +459,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
519 459
520 this.player.one('ended', () => { 460 this.player.one('ended', () => {
521 if (this.playlist) { 461 if (this.playlist) {
522 this.zone.run(() => this.navigateToNextPlaylistVideo()) 462 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
523 } 463 }
524 }) 464 })
525 465
526 this.player.one('stopped', () => { 466 this.player.one('stopped', () => {
527 if (this.playlist) { 467 if (this.playlist) {
528 this.zone.run(() => this.navigateToNextPlaylistVideo()) 468 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
529 } 469 }
530 }) 470 })
531 471
@@ -586,20 +526,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
586 this.setVideoLikesBarTooltipText() 526 this.setVideoLikesBarTooltipText()
587 } 527 }
588 528
589 private updatePlaylistIndex () {
590 if (this.playlistVideos.length === 0 || !this.video) return
591
592 for (const video of this.playlistVideos) {
593 if (video.id === this.video.id) {
594 this.currentPlaylistPosition = video.playlistElement.position
595 return
596 }
597 }
598
599 // Load more videos to find our video
600 this.onPlaylistVideosNearOfBottom()
601 }
602
603 private setOpenGraphTags () { 529 private setOpenGraphTags () {
604 this.metaService.setTitle(this.video.name) 530 this.metaService.setTitle(this.video.name)
605 531
@@ -639,14 +565,4 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
639 this.player = undefined 565 this.player = undefined
640 } 566 }
641 } 567 }
642
643 private navigateToNextPlaylistVideo () {
644 if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
645 const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1)
646
647 const start = next.playlistElement.startTimestamp
648 const stop = next.playlistElement.stopTimestamp
649 this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } })
650 }
651 }
652} 568}
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 983350f52..67596a3da 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -11,6 +11,7 @@ import { VideoWatchComponent } from './video-watch.component'
11import { NgxQRCodeModule } from 'ngx-qrcode2' 11import { NgxQRCodeModule } from 'ngx-qrcode2'
12import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' 12import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
13import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' 13import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
14import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
14 15
15@NgModule({ 16@NgModule({
16 imports: [ 17 imports: [
@@ -23,6 +24,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
23 24
24 declarations: [ 25 declarations: [
25 VideoWatchComponent, 26 VideoWatchComponent,
27 VideoWatchPlaylistComponent,
26 28
27 VideoShareComponent, 29 VideoShareComponent,
28 VideoSupportComponent, 30 VideoSupportComponent,