aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+admin/admin.component.html2
-rw-r--r--client/src/app/+admin/admin.module.ts13
-rw-r--r--client/src/app/+admin/follows/follows.component.html6
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts5
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts2
-rw-r--r--client/src/app/+admin/follows/shared/redundancy.service.ts28
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/index.ts1
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html82
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss37
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts178
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html24
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss8
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts11
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts15
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/shared/images/global-icon.component.ts1
-rw-r--r--client/src/app/shared/instance/instance-statistics.component.ts23
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/shared/video/redundancy.service.ts73
-rw-r--r--client/src/app/shared/video/video-actions-dropdown.component.ts28
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts3
-rw-r--r--client/src/app/shared/video/video.model.ts19
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html5
-rw-r--r--client/src/assets/player/bezels/bezels-plugin.ts86
-rw-r--r--client/src/assets/player/bezels/pause-bezel.ts72
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts23
-rw-r--r--client/src/assets/player/peertube-player-manager.ts64
-rw-r--r--client/src/assets/player/peertube-plugin.ts22
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts51
-rw-r--r--client/src/assets/player/upnext/end-card.ts155
-rw-r--r--client/src/assets/player/upnext/upnext-plugin.ts155
-rw-r--r--client/src/assets/player/videojs-components/next-video-button.ts29
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts48
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts20
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts17
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts32
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts36
-rw-r--r--client/src/assets/player/videojs-components/settings-dialog.ts37
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts191
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts159
-rw-r--r--client/src/assets/player/videojs-components/settings-panel-child.ts22
-rw-r--r--client/src/assets/player/videojs-components/settings-panel.ts22
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts20
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts36
45 files changed, 1194 insertions, 677 deletions
diff --git a/client/src/app/+admin/admin.component.html b/client/src/app/+admin/admin.component.html
index 9a3d90c18..0d06aaedc 100644
--- a/client/src/app/+admin/admin.component.html
+++ b/client/src/app/+admin/admin.component.html
@@ -5,7 +5,7 @@
5 </a> 5 </a>
6 6
7 <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page"> 7 <a i18n *ngIf="hasServerFollowRight()" routerLink="/admin/follows" routerLinkActive="active" class="title-page">
8 Manage follows 8 Follows & redundancies
9 </a> 9 </a>
10 10
11 <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page"> 11 <a i18n *ngIf="hasVideoAbusesRight() || hasVideoBlacklistRight()" routerLink="/admin/moderation" routerLinkActive="active" class="title-page">
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 9c56b5750..fdbe70314 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent } from './follows' 8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -16,7 +16,6 @@ import {
16} from './moderation' 16} from './moderation'
17import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 17import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
18import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 18import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
19import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
20import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 19import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
21import { JobsComponent } from '@app/+admin/system/jobs/jobs.component' 20import { JobsComponent } from '@app/+admin/system/jobs/jobs.component'
22import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system' 21import { JobService, LogsComponent, LogsService, SystemComponent } from '@app/+admin/system'
@@ -27,13 +26,18 @@ import { PluginSearchComponent } from '@app/+admin/plugins/plugin-search/plugin-
27import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component' 26import { PluginShowInstalledComponent } from '@app/+admin/plugins/plugin-show-installed/plugin-show-installed.component'
28import { SelectButtonModule } from 'primeng/selectbutton' 27import { SelectButtonModule } from 'primeng/selectbutton'
29import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
30import { ChartModule } from 'primeng/chart'
30 31
31@NgModule({ 32@NgModule({
32 imports: [ 33 imports: [
33 AdminRoutingModule, 34 AdminRoutingModule,
35
36 SharedModule,
37
34 TableModule, 38 TableModule,
35 SelectButtonModule, 39 SelectButtonModule,
36 SharedModule 40 ChartModule
37 ], 41 ],
38 42
39 declarations: [ 43 declarations: [
@@ -44,6 +48,8 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
44 FollowersListComponent, 48 FollowersListComponent,
45 FollowingListComponent, 49 FollowingListComponent,
46 RedundancyCheckboxComponent, 50 RedundancyCheckboxComponent,
51 VideoRedundanciesListComponent,
52 VideoRedundancyInformationComponent,
47 53
48 UsersComponent, 54 UsersComponent,
49 UserCreateComponent, 55 UserCreateComponent,
@@ -78,7 +84,6 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
78 ], 84 ],
79 85
80 providers: [ 86 providers: [
81 RedundancyService,
82 JobService, 87 JobService,
83 LogsService, 88 LogsService,
84 DebugService, 89 DebugService,
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html
index 21d477132..46581daf9 100644
--- a/client/src/app/+admin/follows/follows.component.html
+++ b/client/src/app/+admin/follows/follows.component.html
@@ -1,5 +1,5 @@
1<div class="admin-sub-header"> 1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Manage follows</div> 2 <div i18n class="form-sub-title">Follows & redundancies</div>
3 3
4 <div class="admin-sub-nav"> 4 <div class="admin-sub-nav">
5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a> 5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a>
@@ -7,7 +7,9 @@
7 <a i18n routerLink="following-add" routerLinkActive="active">Follow</a> 7 <a i18n routerLink="following-add" routerLinkActive="active">Follow</a>
8 8
9 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> 9 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a>
10
11 <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a>
10 </div> 12 </div>
11</div> 13</div>
12 14
13<router-outlet></router-outlet> \ No newline at end of file 15<router-outlet></router-outlet>
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts
index e84c79e82..298733eb0 100644
--- a/client/src/app/+admin/follows/follows.routes.ts
+++ b/client/src/app/+admin/follows/follows.routes.ts
@@ -6,6 +6,7 @@ import { FollowingAddComponent } from './following-add'
6import { FollowersListComponent } from './followers-list' 6import { FollowersListComponent } from './followers-list'
7import { UserRight } from '../../../../../shared' 7import { UserRight } from '../../../../../shared'
8import { FollowingListComponent } from './following-list/following-list.component' 8import { FollowingListComponent } from './following-list/following-list.component'
9import { VideoRedundanciesListComponent } from '@app/+admin/follows/video-redundancies-list'
9 10
10export const FollowsRoutes: Routes = [ 11export const FollowsRoutes: Routes = [
11 { 12 {
@@ -47,6 +48,10 @@ export const FollowsRoutes: Routes = [
47 title: 'Add follow' 48 title: 'Add follow'
48 } 49 }
49 } 50 }
51 },
52 {
53 path: 'video-redundancies-list',
54 component: VideoRedundanciesListComponent
50 } 55 }
51 ] 56 ]
52 } 57 }
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts
index e94f33710..4fcb35cb1 100644
--- a/client/src/app/+admin/follows/index.ts
+++ b/client/src/app/+admin/follows/index.ts
@@ -1,5 +1,6 @@
1export * from './following-add' 1export * from './following-add'
2export * from './followers-list' 2export * from './followers-list'
3export * from './following-list' 3export * from './following-list'
4export * from './video-redundancies-list'
4export * from './follows.component' 5export * from './follows.component'
5export * from './follows.routes' 6export * from './follows.routes'
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
index fa1da26bf..9d7883d97 100644
--- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
+++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts
@@ -1,7 +1,7 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 4import { RedundancyService } from '@app/shared/video/redundancy.service'
5 5
6@Component({ 6@Component({
7 selector: 'my-redundancy-checkbox', 7 selector: 'my-redundancy-checkbox',
diff --git a/client/src/app/+admin/follows/shared/redundancy.service.ts b/client/src/app/+admin/follows/shared/redundancy.service.ts
deleted file mode 100644
index 87ae01c04..000000000
--- a/client/src/app/+admin/follows/shared/redundancy.service.ts
+++ /dev/null
@@ -1,28 +0,0 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/shared'
5import { environment } from '../../../../environments/environment'
6
7@Injectable()
8export class RedundancyService {
9 static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/server/redundancy'
10
11 constructor (
12 private authHttp: HttpClient,
13 private restExtractor: RestExtractor
14 ) { }
15
16 updateRedundancy (host: string, redundancyAllowed: boolean) {
17 const url = RedundancyService.BASE_USER_SUBSCRIPTIONS_URL + '/' + host
18
19 const body = { redundancyAllowed }
20
21 return this.authHttp.put(url, body)
22 .pipe(
23 map(this.restExtractor.extractDataBool),
24 catchError(err => this.restExtractor.handleError(err))
25 )
26 }
27
28}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/index.ts b/client/src/app/+admin/follows/video-redundancies-list/index.ts
new file mode 100644
index 000000000..6a7c7f483
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/index.ts
@@ -0,0 +1 @@
export * from './video-redundancies-list.component'
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
new file mode 100644
index 000000000..80c66ec60
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
@@ -0,0 +1,82 @@
1<div class="admin-sub-header">
2 <div i18n class="form-sub-title">Video redundancies list</div>
3
4 <div class="select-filter-block">
5 <label for="displayType" i18n>Display</label>
6
7 <div class="peertube-select-container">
8 <select id="displayType" name="displayType" [(ngModel)]="displayType" (ngModelChange)="onDisplayTypeChanged()">
9 <option value="my-videos">My videos duplicated by remote instances</option>
10 <option value="remote-videos">Remote videos duplicated by my instance</option>
11 </select>
12 </div>
13 </div>
14</div>
15
16<p-table
17 [value]="videoRedundancies" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
18 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
19>
20 <ng-template pTemplate="header">
21 <tr>
22 <th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
23 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
24 <th i18n>Video URL</th>
25 <th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
26 <th></th>
27 </tr>
28 </ng-template>
29
30 <ng-template pTemplate="body" let-redundancy>
31 <tr class="expander" [pRowToggler]="redundancy">
32 <td *ngIf="isDisplayingRemoteVideos()">{{ getRedundancyStrategy(redundancy) }}</td>
33
34 <td>{{ redundancy.name }}</td>
35
36 <td>
37 <a target="_blank" rel="noopener noreferrer" [href]="redundancy.url">{{ redundancy.url }}</a>
38 </td>
39
40 <td *ngIf="isDisplayingRemoteVideos()">{{ getTotalSize(redundancy) | bytes: 1 }}</td>
41
42 <td class="action-cell">
43 <my-delete-button (click)="removeRedundancy(redundancy)"></my-delete-button>
44 </td>
45 </tr>
46 </ng-template>
47
48 <ng-template pTemplate="rowexpansion" let-redundancy>
49 <tr>
50 <td colspan="2">
51 <div *ngFor="let file of redundancy.redundancies.files" class="expansion-block">
52 <my-video-redundancy-information [redundancyElement]="file"></my-video-redundancy-information>
53 </div>
54 </td>
55 </tr>
56
57 <tr>
58 <td colspan="2">
59 <div *ngFor="let playlist of redundancy.redundancies.streamingPlaylists">
60 <my-video-redundancy-information [redundancyElement]="playlist"></my-video-redundancy-information>
61 </div>
62 </td>
63 </tr>
64 </ng-template>
65</p-table>
66
67
68<div class="redundancies-charts" *ngIf="isDisplayingRemoteVideos()">
69 <div class="form-sub-title" i18n>Enabled strategies stats</div>
70
71 <div class="chart-blocks">
72
73 <div *ngIf="noRedundancies" i18n class="no-results">
74 No redundancy strategy is enabled on your instance.
75 </div>
76
77 <div class="chart-block" *ngFor="let r of redundanciesGraphsData">
78 <p-chart type="pie" [data]="r.graphData" [options]="r.options" width="300px" height="300px"></p-chart>
79 </div>
80
81 </div>
82</div>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss
new file mode 100644
index 000000000..05018c281
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.scss
@@ -0,0 +1,37 @@
1@import '_variables';
2@import '_mixins';
3
4.expansion-block {
5 margin-bottom: 20px;
6}
7
8.admin-sub-header {
9 align-items: flex-end;
10
11 .select-filter-block {
12 &:not(:last-child) {
13 margin-right: 10px;
14 }
15
16 label {
17 margin-bottom: 2px;
18 }
19
20 .peertube-select-container {
21 @include peertube-select-container(auto);
22 }
23 }
24}
25
26.redundancies-charts {
27 margin-top: 50px;
28
29 .chart-blocks {
30 display: flex;
31 justify-content: center;
32
33 .chart-block {
34 margin: 0 20px;
35 }
36 }
37}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
new file mode 100644
index 000000000..4b41d1d86
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
@@ -0,0 +1,178 @@
1import { Component, OnInit } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { SortMeta } from 'primeng/api'
4import { ConfirmService } from '../../../core/confirm/confirm.service'
5import { RestPagination, RestTable } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
8import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
9import { VideosRedundancyStats } from '@shared/models/server'
10import { BytesPipe } from 'ngx-pipes'
11import { RedundancyService } from '@app/shared/video/redundancy.service'
12
13@Component({
14 selector: 'my-video-redundancies-list',
15 templateUrl: './video-redundancies-list.component.html',
16 styleUrls: [ './video-redundancies-list.component.scss' ]
17})
18export class VideoRedundanciesListComponent extends RestTable implements OnInit {
19 private static LOCAL_STORAGE_DISPLAY_TYPE = 'video-redundancies-list-display-type'
20
21 videoRedundancies: VideoRedundancy[] = []
22 totalRecords = 0
23 rowsPerPage = 10
24
25 sort: SortMeta = { field: 'name', order: 1 }
26 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
27 displayType: VideoRedundanciesTarget = 'my-videos'
28
29 redundanciesGraphsData: { stats: VideosRedundancyStats, graphData: object, options: object }[] = []
30
31 noRedundancies = false
32
33 private bytesPipe: BytesPipe
34
35 constructor (
36 private notifier: Notifier,
37 private confirmService: ConfirmService,
38 private redundancyService: RedundancyService,
39 private serverService: ServerService,
40 private i18n: I18n
41 ) {
42 super()
43
44 this.bytesPipe = new BytesPipe()
45 }
46
47 ngOnInit () {
48 this.loadSelectLocalStorage()
49
50 this.initialize()
51
52 this.serverService.getServerStats()
53 .subscribe(res => {
54 const redundancies = res.videosRedundancy
55
56 if (redundancies.length === 0) this.noRedundancies = true
57
58 for (const r of redundancies) {
59 this.buildPieData(r)
60 }
61 })
62 }
63
64 isDisplayingRemoteVideos () {
65 return this.displayType === 'remote-videos'
66 }
67
68 getTotalSize (redundancy: VideoRedundancy) {
69 return redundancy.redundancies.files.reduce((a, b) => a + b.size, 0) +
70 redundancy.redundancies.streamingPlaylists.reduce((a, b) => a + b.size, 0)
71 }
72
73 onDisplayTypeChanged () {
74 this.pagination.start = 0
75 this.saveSelectLocalStorage()
76
77 this.loadData()
78 }
79
80 getRedundancyStrategy (redundancy: VideoRedundancy) {
81 if (redundancy.redundancies.files.length !== 0) return redundancy.redundancies.files[0].strategy
82 if (redundancy.redundancies.streamingPlaylists.length !== 0) return redundancy.redundancies.streamingPlaylists[0].strategy
83
84 return ''
85 }
86
87 buildPieData (stats: VideosRedundancyStats) {
88 const totalSize = stats.totalSize
89 ? stats.totalSize - stats.totalUsed
90 : stats.totalUsed
91
92 if (totalSize === 0) return
93
94 this.redundanciesGraphsData.push({
95 stats,
96 graphData: {
97 labels: [ this.i18n('Used'), this.i18n('Available') ],
98 datasets: [
99 {
100 data: [ stats.totalUsed, totalSize ],
101 backgroundColor: [
102 '#FF6384',
103 '#36A2EB'
104 ],
105 hoverBackgroundColor: [
106 '#FF6384',
107 '#36A2EB'
108 ]
109 }
110 ]
111 },
112 options: {
113 title: {
114 display: true,
115 text: stats.strategy
116 },
117
118 tooltips: {
119 callbacks: {
120 label: (tooltipItem: any, data: any) => {
121 const dataset = data.datasets[tooltipItem.datasetIndex]
122 let label = data.labels[tooltipItem.index]
123 if (label) label += ': '
124 else label = ''
125
126 label += this.bytesPipe.transform(dataset.data[tooltipItem.index], 1)
127 return label
128 }
129 }
130 }
131 }
132 })
133 }
134
135 async removeRedundancy (redundancy: VideoRedundancy) {
136 const message = this.i18n('Do you really want to remove this video redundancy?')
137 const res = await this.confirmService.confirm(message, this.i18n('Remove redundancy'))
138 if (res === false) return
139
140 this.redundancyService.removeVideoRedundancies(redundancy)
141 .subscribe(
142 () => {
143 this.notifier.success(this.i18n('Video redundancies removed!'))
144 this.loadData()
145 },
146
147 err => this.notifier.error(err.message)
148 )
149
150 }
151
152 protected loadData () {
153 const options = {
154 pagination: this.pagination,
155 sort: this.sort,
156 target: this.displayType
157 }
158
159 this.redundancyService.listVideoRedundancies(options)
160 .subscribe(
161 resultList => {
162 this.videoRedundancies = resultList.data
163 this.totalRecords = resultList.total
164 },
165
166 err => this.notifier.error(err.message)
167 )
168 }
169
170 private loadSelectLocalStorage () {
171 const displayType = peertubeLocalStorage.getItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE)
172 if (displayType) this.displayType = displayType as VideoRedundanciesTarget
173 }
174
175 private saveSelectLocalStorage () {
176 peertubeLocalStorage.setItem(VideoRedundanciesListComponent.LOCAL_STORAGE_DISPLAY_TYPE, this.displayType)
177 }
178}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html
new file mode 100644
index 000000000..a379520e3
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.html
@@ -0,0 +1,24 @@
1<div>
2 <span class="label">Url</span>
3 <a target="_blank" rel="noopener noreferrer" [href]="redundancyElement.fileUrl">{{ redundancyElement.fileUrl }}</a>
4</div>
5
6<div>
7 <span class="label">Created on</span>
8 <span>{{ redundancyElement.createdAt | date: 'medium' }}</span>
9</div>
10
11<div>
12 <span class="label">Expires on</span>
13 <span>{{ redundancyElement.expiresOn | date: 'medium' }}</span>
14</div>
15
16<div>
17 <span class="label">Size</span>
18 <span>{{ redundancyElement.size | bytes: 1 }}</span>
19</div>
20
21<div *ngIf="redundancyElement.strategy">
22 <span class="label">Strategy</span>
23 <span>{{ redundancyElement.strategy }}</span>
24</div>
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss
new file mode 100644
index 000000000..6b09fbb01
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.scss
@@ -0,0 +1,8 @@
1@import '_variables';
2@import '_mixins';
3
4.label {
5 display: inline-block;
6 min-width: 100px;
7 font-weight: $font-semibold;
8}
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts
new file mode 100644
index 000000000..6f3090c08
--- /dev/null
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancy-information.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { FileRedundancyInformation, StreamingPlaylistRedundancyInformation } from '@shared/models'
3
4@Component({
5 selector: 'my-video-redundancy-information',
6 templateUrl: './video-redundancy-information.component.html',
7 styleUrls: [ './video-redundancy-information.component.scss' ]
8})
9export class VideoRedundancyInformationComponent {
10 @Input() redundancyElement: FileRedundancyInformation | StreamingPlaylistRedundancyInformation
11}
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index 20c8ea71a..bc40452cf 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -16,8 +16,8 @@ import { JobTypeClient } from '../../../../types/job-type-client.type'
16 styleUrls: [ './jobs.component.scss' ] 16 styleUrls: [ './jobs.component.scss' ]
17}) 17})
18export class JobsComponent extends RestTable implements OnInit { 18export class JobsComponent extends RestTable implements OnInit {
19 private static JOB_STATE_LOCAL_STORAGE_STATE = 'jobs-list-state' 19 private static LOCAL_STORAGE_STATE = 'jobs-list-state'
20 private static JOB_STATE_LOCAL_STORAGE_TYPE = 'jobs-list-type' 20 private static LOCAL_STORAGE_TYPE = 'jobs-list-type'
21 21
22 jobState: JobStateClient = 'waiting' 22 jobState: JobStateClient = 'waiting'
23 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ] 23 jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ]
@@ -34,7 +34,8 @@ export class JobsComponent extends RestTable implements OnInit {
34 'video-file-import', 34 'video-file-import',
35 'video-import', 35 'video-import',
36 'videos-views', 36 'videos-views',
37 'activitypub-refresher' 37 'activitypub-refresher',
38 'video-redundancy'
38 ] 39 ]
39 40
40 jobs: Job[] = [] 41 jobs: Job[] = []
@@ -77,15 +78,15 @@ export class JobsComponent extends RestTable implements OnInit {
77 } 78 }
78 79
79 private loadJobStateAndType () { 80 private loadJobStateAndType () {
80 const state = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE) 81 const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE)
81 if (state) this.jobState = state as JobState 82 if (state) this.jobState = state as JobState
82 83
83 const type = peertubeLocalStorage.getItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE) 84 const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE)
84 if (type) this.jobType = type as JobType 85 if (type) this.jobType = type as JobType
85 } 86 }
86 87
87 private saveJobStateAndType () { 88 private saveJobStateAndType () {
88 peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_STATE, this.jobState) 89 peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_STATE, this.jobState)
89 peertubeLocalStorage.setItem(JobsComponent.JOB_STATE_LOCAL_STORAGE_TYPE, this.jobType) 90 peertubeLocalStorage.setItem(JobsComponent.LOCAL_STORAGE_TYPE, this.jobType)
90 } 91 }
91} 92}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index cdcbcb528..1f6cfb596 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -9,6 +9,7 @@ import { VideoConstant } from '../../../../../shared/models/videos'
9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n' 9import { isDefaultLocale, peertubeTranslate } from '../../../../../shared/models/i18n'
10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 10import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
11import { sortBy } from '@app/shared/misc/utils' 11import { sortBy } from '@app/shared/misc/utils'
12import { ServerStats } from '@shared/models/server'
12 13
13@Injectable() 14@Injectable()
14export class ServerService { 15export class ServerService {
@@ -16,6 +17,8 @@ export class ServerService {
16 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' 17 private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
17 private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' 18 private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/'
18 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' 19 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
20 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
21
19 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' 22 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
20 23
21 configReloaded = new Subject<void>() 24 configReloaded = new Subject<void>()
@@ -235,6 +238,10 @@ export class ServerService {
235 return this.localeObservable.pipe(first()) 238 return this.localeObservable.pipe(first())
236 } 239 }
237 240
241 getServerStats () {
242 return this.http.get<ServerStats>(ServerService.BASE_STATS_URL)
243 }
244
238 private loadAttributeEnum <T extends string | number> ( 245 private loadAttributeEnum <T extends string | number> (
239 baseUrl: string, 246 baseUrl: string,
240 attributeName: 'categories' | 'licences' | 'languages' | 'privacies', 247 attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts
index 806aca347..b6e641228 100644
--- a/client/src/app/shared/images/global-icon.component.ts
+++ b/client/src/app/shared/images/global-icon.component.ts
@@ -1,6 +1,5 @@
1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core' 1import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'
2import { HooksService } from '@app/core/plugins/hooks.service' 2import { HooksService } from '@app/core/plugins/hooks.service'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4 3
5const icons = { 4const icons = {
6 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'), 5 'add': require('!!raw-loader?!../../../assets/images/global/add.svg'),
diff --git a/client/src/app/shared/instance/instance-statistics.component.ts b/client/src/app/shared/instance/instance-statistics.component.ts
index 8ec728f05..40aa8a4c0 100644
--- a/client/src/app/shared/instance/instance-statistics.component.ts
+++ b/client/src/app/shared/instance/instance-statistics.component.ts
@@ -1,9 +1,6 @@
1import { map } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { ServerStats } from '@shared/models/server' 2import { ServerStats } from '@shared/models/server'
6import { environment } from '../../../environments/environment' 3import { ServerService } from '@app/core'
7 4
8@Component({ 5@Component({
9 selector: 'my-instance-statistics', 6 selector: 'my-instance-statistics',
@@ -11,27 +8,15 @@ import { environment } from '../../../environments/environment'
11 styleUrls: [ './instance-statistics.component.scss' ] 8 styleUrls: [ './instance-statistics.component.scss' ]
12}) 9})
13export class InstanceStatisticsComponent implements OnInit { 10export class InstanceStatisticsComponent implements OnInit {
14 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
15
16 serverStats: ServerStats = null 11 serverStats: ServerStats = null
17 12
18 constructor ( 13 constructor (
19 private http: HttpClient, 14 private serverService: ServerService
20 private i18n: I18n
21 ) { 15 ) {
22 } 16 }
23 17
24 ngOnInit () { 18 ngOnInit () {
25 this.getStats() 19 this.serverService.getServerStats()
26 .subscribe( 20 .subscribe(res => this.serverStats = res)
27 res => {
28 this.serverStats = res
29 }
30 )
31 }
32
33 getStats () {
34 return this.http
35 .get<ServerStats>(InstanceStatisticsComponent.BASE_STATS_URL)
36 } 21 }
37} 22}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b2eb13f73..d06d37d8c 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -98,6 +98,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
98import { MultiSelectModule } from 'primeng/multiselect' 98import { MultiSelectModule } from 'primeng/multiselect'
99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component' 99import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component' 100import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
101import { RedundancyService } from '@app/shared/video/redundancy.service'
101 102
102@NgModule({ 103@NgModule({
103 imports: [ 104 imports: [
@@ -300,6 +301,7 @@ import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-cop
300 UserNotificationService, 301 UserNotificationService,
301 302
302 FollowService, 303 FollowService,
304 RedundancyService,
303 305
304 I18n 306 I18n
305 ] 307 ]
diff --git a/client/src/app/shared/video/redundancy.service.ts b/client/src/app/shared/video/redundancy.service.ts
new file mode 100644
index 000000000..fb918d73b
--- /dev/null
+++ b/client/src/app/shared/video/redundancy.service.ts
@@ -0,0 +1,73 @@
1import { catchError, map, toArray } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, RestPagination, RestService } from '@app/shared/rest'
5import { SortMeta } from 'primeng/api'
6import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
7import { concat, Observable } from 'rxjs'
8import { environment } from '../../../environments/environment'
9
10@Injectable()
11export class RedundancyService {
12 static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy'
13
14 constructor (
15 private authHttp: HttpClient,
16 private restService: RestService,
17 private restExtractor: RestExtractor
18 ) { }
19
20 updateRedundancy (host: string, redundancyAllowed: boolean) {
21 const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host
22
23 const body = { redundancyAllowed }
24
25 return this.authHttp.put(url, body)
26 .pipe(
27 map(this.restExtractor.extractDataBool),
28 catchError(err => this.restExtractor.handleError(err))
29 )
30 }
31
32 listVideoRedundancies (options: {
33 pagination: RestPagination,
34 sort: SortMeta,
35 target?: VideoRedundanciesTarget
36 }): Observable<ResultList<VideoRedundancy>> {
37 const { pagination, sort, target } = options
38
39 let params = new HttpParams()
40 params = this.restService.addRestGetParams(params, pagination, sort)
41
42 if (target) params = params.append('target', target)
43
44 return this.authHttp.get<ResultList<VideoRedundancy>>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params })
45 .pipe(
46 catchError(res => this.restExtractor.handleError(res))
47 )
48 }
49
50 addVideoRedundancy (video: Video) {
51 return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id })
52 .pipe(
53 catchError(res => this.restExtractor.handleError(res))
54 )
55 }
56
57 removeVideoRedundancies (redundancy: VideoRedundancy) {
58 const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id)
59 .concat(redundancy.redundancies.files.map(r => r.id))
60 .map(id => this.removeRedundancy(id))
61
62 return concat(...observables)
63 .pipe(toArray())
64 }
65
66 private removeRedundancy (redundancyId: number) {
67 return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId)
68 .pipe(
69 map(this.restExtractor.extractDataBool),
70 catchError(res => this.restExtractor.handleError(res))
71 )
72 }
73}
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 afdeab18d..390d74c52 100644
--- a/client/src/app/shared/video/video-actions-dropdown.component.ts
+++ b/client/src/app/shared/video/video-actions-dropdown.component.ts
@@ -14,6 +14,7 @@ import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklis
14import { VideoBlacklistService } from '@app/shared/video-blacklist' 14import { VideoBlacklistService } from '@app/shared/video-blacklist'
15import { ScreenService } from '@app/shared/misc/screen.service' 15import { ScreenService } from '@app/shared/misc/screen.service'
16import { VideoCaption } from '@shared/models' 16import { VideoCaption } from '@shared/models'
17import { RedundancyService } from '@app/shared/video/redundancy.service'
17 18
18export type VideoActionsDisplayType = { 19export type VideoActionsDisplayType = {
19 playlist?: boolean 20 playlist?: boolean
@@ -22,6 +23,7 @@ export type VideoActionsDisplayType = {
22 blacklist?: boolean 23 blacklist?: boolean
23 delete?: boolean 24 delete?: boolean
24 report?: boolean 25 report?: boolean
26 duplicate?: boolean
25} 27}
26 28
27@Component({ 29@Component({
@@ -46,7 +48,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
46 update: true, 48 update: true,
47 blacklist: true, 49 blacklist: true,
48 delete: true, 50 delete: true,
49 report: true 51 report: true,
52 duplicate: true
50 } 53 }
51 @Input() placement = 'left' 54 @Input() placement = 'left'
52 55
@@ -74,6 +77,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
74 private screenService: ScreenService, 77 private screenService: ScreenService,
75 private videoService: VideoService, 78 private videoService: VideoService,
76 private blocklistService: BlocklistService, 79 private blocklistService: BlocklistService,
80 private redundancyService: RedundancyService,
77 private i18n: I18n 81 private i18n: I18n
78 ) { } 82 ) { }
79 83
@@ -144,6 +148,10 @@ export class VideoActionsDropdownComponent implements OnChanges {
144 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled 148 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
145 } 149 }
146 150
151 canVideoBeDuplicated () {
152 return this.video.canBeDuplicatedBy(this.user)
153 }
154
147 /* Action handlers */ 155 /* Action handlers */
148 156
149 async unblacklistVideo () { 157 async unblacklistVideo () {
@@ -186,6 +194,18 @@ export class VideoActionsDropdownComponent implements OnChanges {
186 ) 194 )
187 } 195 }
188 196
197 duplicateVideo () {
198 this.redundancyService.addVideoRedundancy(this.video)
199 .subscribe(
200 () => {
201 const message = this.i18n('This video will be duplicated by your instance.')
202 this.notifier.success(message)
203 },
204
205 err => this.notifier.error(err.message)
206 )
207 }
208
189 onVideoBlacklisted () { 209 onVideoBlacklisted () {
190 this.videoBlacklisted.emit() 210 this.videoBlacklisted.emit()
191 } 211 }
@@ -234,6 +254,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
234 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable() 254 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.blacklist && this.isVideoUnblacklistable()
235 }, 255 },
236 { 256 {
257 label: this.i18n('Duplicate (redundancy)'),
258 handler: () => this.duplicateVideo(),
259 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.duplicate && this.canVideoBeDuplicated(),
260 iconName: 'cloud-download'
261 },
262 {
237 label: this.i18n('Delete'), 263 label: this.i18n('Delete'),
238 handler: () => this.removeVideo(), 264 handler: () => this.removeVideo(),
239 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(), 265 isDisplayed: () => this.authService.isLoggedIn() && this.displayOptions.delete && this.isVideoRemovable(),
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts
index 598a7a983..1dfb3eec7 100644
--- a/client/src/app/shared/video/video-miniature.component.ts
+++ b/client/src/app/shared/video/video-miniature.component.ts
@@ -64,7 +64,8 @@ export class VideoMiniatureComponent implements OnInit {
64 update: true, 64 update: true,
65 blacklist: true, 65 blacklist: true,
66 delete: true, 66 delete: true,
67 report: true 67 report: true,
68 duplicate: false
68 } 69 }
69 showActions = false 70 showActions = false
70 serverConfig: ServerConfig 71 serverConfig: ServerConfig
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index fb98d5382..546518cca 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -42,6 +42,9 @@ export class Video implements VideoServerModel {
42 dislikes: number 42 dislikes: number
43 nsfw: boolean 43 nsfw: boolean
44 44
45 originInstanceUrl: string
46 originInstanceHost: string
47
45 waitTranscoding?: boolean 48 waitTranscoding?: boolean
46 state?: VideoConstant<VideoState> 49 state?: VideoConstant<VideoState>
47 scheduledUpdate?: VideoScheduleUpdate 50 scheduledUpdate?: VideoScheduleUpdate
@@ -86,22 +89,31 @@ export class Video implements VideoServerModel {
86 this.waitTranscoding = hash.waitTranscoding 89 this.waitTranscoding = hash.waitTranscoding
87 this.state = hash.state 90 this.state = hash.state
88 this.description = hash.description 91 this.description = hash.description
92
89 this.duration = hash.duration 93 this.duration = hash.duration
90 this.durationLabel = durationToString(hash.duration) 94 this.durationLabel = durationToString(hash.duration)
95
91 this.id = hash.id 96 this.id = hash.id
92 this.uuid = hash.uuid 97 this.uuid = hash.uuid
98
93 this.isLocal = hash.isLocal 99 this.isLocal = hash.isLocal
94 this.name = hash.name 100 this.name = hash.name
101
95 this.thumbnailPath = hash.thumbnailPath 102 this.thumbnailPath = hash.thumbnailPath
96 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath 103 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
104
97 this.previewPath = hash.previewPath 105 this.previewPath = hash.previewPath
98 this.previewUrl = absoluteAPIUrl + hash.previewPath 106 this.previewUrl = absoluteAPIUrl + hash.previewPath
107
99 this.embedPath = hash.embedPath 108 this.embedPath = hash.embedPath
100 this.embedUrl = absoluteAPIUrl + hash.embedPath 109 this.embedUrl = absoluteAPIUrl + hash.embedPath
110
101 this.views = hash.views 111 this.views = hash.views
102 this.likes = hash.likes 112 this.likes = hash.likes
103 this.dislikes = hash.dislikes 113 this.dislikes = hash.dislikes
114
104 this.nsfw = hash.nsfw 115 this.nsfw = hash.nsfw
116
105 this.account = hash.account 117 this.account = hash.account
106 this.channel = hash.channel 118 this.channel = hash.channel
107 119
@@ -124,6 +136,9 @@ export class Video implements VideoServerModel {
124 this.blacklistedReason = hash.blacklistedReason 136 this.blacklistedReason = hash.blacklistedReason
125 137
126 this.userHistory = hash.userHistory 138 this.userHistory = hash.userHistory
139
140 this.originInstanceHost = this.account.host
141 this.originInstanceUrl = 'https://' + this.originInstanceHost
127 } 142 }
128 143
129 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 144 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
@@ -152,4 +167,8 @@ export class Video implements VideoServerModel {
152 isUpdatableBy (user: AuthUser) { 167 isUpdatableBy (user: AuthUser) {
153 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) 168 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
154 } 169 }
170
171 canBeDuplicatedBy (user: AuthUser) {
172 return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES)
173 }
155} 174}
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 bc3a3ffdd..a382777f5 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -188,6 +188,11 @@
188 <span class="video-attribute-value">{{ video.privacy.label }}</span> 188 <span class="video-attribute-value">{{ video.privacy.label }}</span>
189 </div> 189 </div>
190 190
191 <div *ngIf="video.isLocal === false" class="video-attribute">
192 <span i18n class="video-attribute-label">Origin instance</span>
193 <a class="video-attribute-value" target="_blank" rel="noopener noreferrer" [href]="video.originInstanceUrl">{{ video.originInstanceHost }}</a>
194 </div>
195
191 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute"> 196 <div *ngIf="!!video.originallyPublishedAt" class="video-attribute">
192 <span i18n class="video-attribute-label">Originally published</span> 197 <span i18n class="video-attribute-label">Originally published</span>
193 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> 198 <span class="video-attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
diff --git a/client/src/assets/player/bezels/bezels-plugin.ts b/client/src/assets/player/bezels/bezels-plugin.ts
index c2c251961..499177526 100644
--- a/client/src/assets/player/bezels/bezels-plugin.ts
+++ b/client/src/assets/player/bezels/bezels-plugin.ts
@@ -1,85 +1,12 @@
1// @ts-ignore 1import videojs, { VideoJsPlayer } from 'video.js'
2import * as videojs from 'video.js' 2import './pause-bezel'
3import { VideoJSComponentInterface } from '../peertube-videojs-typings'
4 3
5function getPauseBezel () { 4const Plugin = videojs.getPlugin('plugin')
6 return `
7 <div class="vjs-bezels-pause">
8 <div class="vjs-bezel" role="status" aria-label="Pause">
9 <div class="vjs-bezel-icon">
10 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
11 <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use>
12 <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path>
13 </svg>
14 </div>
15 </div>
16 </div>
17 `
18}
19
20function getPlayBezel () {
21 return `
22 <div class="vjs-bezels-play">
23 <div class="vjs-bezel" role="status" aria-label="Play">
24 <div class="vjs-bezel-icon">
25 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
26 <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use>
27 <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path>
28 </svg>
29 </div>
30 </div>
31 </div>
32 `
33}
34
35// @ts-ignore-start
36const Component = videojs.getComponent('Component')
37class PauseBezel extends Component {
38 options_: any
39 container: HTMLBodyElement
40
41 constructor (player: videojs.Player, options: any) {
42 super(player, options)
43 this.options_ = options
44
45 player.on('pause', (_: any) => {
46 if (player.seeking() || player.ended()) return
47 this.container.innerHTML = getPauseBezel()
48 this.showBezel()
49 })
50
51 player.on('play', (_: any) => {
52 if (player.seeking()) return
53 this.container.innerHTML = getPlayBezel()
54 this.showBezel()
55 })
56 }
57 5
58 createEl () {
59 const container = super.createEl('div', {
60 className: 'vjs-bezels-content'
61 })
62 this.container = container
63 container.style.display = 'none'
64
65 return container
66 }
67
68 showBezel () {
69 this.container.style.display = 'inherit'
70 setTimeout(() => {
71 this.container.style.display = 'none'
72 }, 500) // matching the animation duration
73 }
74}
75// @ts-ignore-end
76
77videojs.registerComponent('PauseBezel', PauseBezel)
78
79const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
80class BezelsPlugin extends Plugin { 6class BezelsPlugin extends Plugin {
81 constructor (player: videojs.Player, options: any = {}) { 7
82 super(player, options) 8 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
9 super(player)
83 10
84 this.player.ready(() => { 11 this.player.ready(() => {
85 player.addClass('vjs-bezels') 12 player.addClass('vjs-bezels')
@@ -90,4 +17,5 @@ class BezelsPlugin extends Plugin {
90} 17}
91 18
92videojs.registerPlugin('bezels', BezelsPlugin) 19videojs.registerPlugin('bezels', BezelsPlugin)
20
93export { BezelsPlugin } 21export { BezelsPlugin }
diff --git a/client/src/assets/player/bezels/pause-bezel.ts b/client/src/assets/player/bezels/pause-bezel.ts
new file mode 100644
index 000000000..98eb12099
--- /dev/null
+++ b/client/src/assets/player/bezels/pause-bezel.ts
@@ -0,0 +1,72 @@
1import videojs, { VideoJsPlayer } from 'video.js'
2
3function getPauseBezel () {
4 return `
5 <div class="vjs-bezels-pause">
6 <div class="vjs-bezel" role="status" aria-label="Pause">
7 <div class="vjs-bezel-icon">
8 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
9 <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use>
10 <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path>
11 </svg>
12 </div>
13 </div>
14 </div>
15 `
16}
17
18function getPlayBezel () {
19 return `
20 <div class="vjs-bezels-play">
21 <div class="vjs-bezel" role="status" aria-label="Play">
22 <div class="vjs-bezel-icon">
23 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
24 <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use>
25 <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path>
26 </svg>
27 </div>
28 </div>
29 </div>
30 `
31}
32
33const Component = videojs.getComponent('Component')
34class PauseBezel extends Component {
35 container: HTMLDivElement
36
37 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
38 super(player, options)
39
40 player.on('pause', (_: any) => {
41 if (player.seeking() || player.ended()) return
42 this.container.innerHTML = getPauseBezel()
43 this.showBezel()
44 })
45
46 player.on('play', (_: any) => {
47 if (player.seeking()) return
48 this.container.innerHTML = getPlayBezel()
49 this.showBezel()
50 })
51 }
52
53 createEl () {
54 this.container = super.createEl('div', {
55 className: 'vjs-bezels-content'
56 }) as HTMLDivElement
57
58 this.container.style.display = 'none'
59
60 return this.container
61 }
62
63 showBezel () {
64 this.container.style.display = 'inherit'
65
66 setTimeout(() => {
67 this.container.style.display = 'none'
68 }, 500) // matching the animation duration
69 }
70}
71
72videojs.registerComponent('PauseBezel', PauseBezel)
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index c3f863f72..512054ae6 100644
--- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -1,7 +1,5 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore 2import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings'
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' 3import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events, Segment } from 'p2p-media-loader-core' 4import { Events, Segment } from 'p2p-media-loader-core'
7import { timeToInt } from '../utils' 5import { timeToInt } from '../utils'
@@ -10,7 +8,7 @@ import { timeToInt } from '../utils'
10window['videojs'] = videojs 8window['videojs'] = videojs
11require('@streamroot/videojs-hlsjs-plugin') 9require('@streamroot/videojs-hlsjs-plugin')
12 10
13const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 11const Plugin = videojs.getPlugin('plugin')
14class P2pMediaLoaderPlugin extends Plugin { 12class P2pMediaLoaderPlugin extends Plugin {
15 13
16 private readonly CONSTANTS = { 14 private readonly CONSTANTS = {
@@ -37,12 +35,13 @@ class P2pMediaLoaderPlugin extends Plugin {
37 35
38 private networkInfoInterval: any 36 private networkInfoInterval: any
39 37
40 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { 38 constructor (player: VideoJsPlayer, options?: P2PMediaLoaderPluginOptions) {
41 super(player, options) 39 super(player)
42 40
43 this.options = options 41 this.options = options
44 42
45 if (!videojs.Html5Hlsjs) { 43 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
44 if (!(videojs as any).Html5Hlsjs) {
46 const message = 'HLS.js does not seem to be supported.' 45 const message = 'HLS.js does not seem to be supported.'
47 console.warn(message) 46 console.warn(message)
48 47
@@ -50,7 +49,8 @@ class P2pMediaLoaderPlugin extends Plugin {
50 return 49 return
51 } 50 }
52 51
53 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { 52 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
53 (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
54 this.hlsjs = hlsjs 54 this.hlsjs = hlsjs
55 }) 55 })
56 56
@@ -84,8 +84,9 @@ class P2pMediaLoaderPlugin extends Plugin {
84 private initialize () { 84 private initialize () {
85 initHlsJsPlayer(this.hlsjs) 85 initHlsJsPlayer(this.hlsjs)
86 86
87 const tech = this.player.tech_ 87 // FIXME: typings
88 this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() 88 const options = this.player.tech(true).options_ as any
89 this.p2pEngine = options.hlsjsConfig.loader.getEngine()
89 90
90 // Avoid using constants to not import hls.hs 91 // Avoid using constants to not import hls.hs
91 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 92 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index d9e02cd7d..4e6387a53 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -1,21 +1,26 @@
1import { VideoFile } from '../../../../shared/models/videos' 1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore 2import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
3import * as videojs from 'video.js'
4import 'videojs-hotkeys' 3import 'videojs-hotkeys'
5import 'videojs-dock' 4import 'videojs-dock'
6import 'videojs-contextmenu-ui' 5import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels' 6import 'videojs-contrib-quality-levels'
7import './upnext/end-card'
8import './upnext/upnext-plugin' 8import './upnext/upnext-plugin'
9import './bezels/bezels-plugin' 9import './bezels/bezels-plugin'
10import './peertube-plugin' 10import './peertube-plugin'
11import './videojs-components/next-video-button' 11import './videojs-components/next-video-button'
12import './videojs-components/p2p-info-button'
12import './videojs-components/peertube-link-button' 13import './videojs-components/peertube-link-button'
14import './videojs-components/peertube-load-progress-bar'
13import './videojs-components/resolution-menu-button' 15import './videojs-components/resolution-menu-button'
16import './videojs-components/resolution-menu-item'
17import './videojs-components/settings-dialog'
14import './videojs-components/settings-menu-button' 18import './videojs-components/settings-menu-button'
15import './videojs-components/p2p-info-button' 19import './videojs-components/settings-menu-item'
16import './videojs-components/peertube-load-progress-bar' 20import './videojs-components/settings-panel'
21import './videojs-components/settings-panel-child'
17import './videojs-components/theater-button' 22import './videojs-components/theater-button'
18import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' 23import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings'
19import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' 24import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
20import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' 25import { isDefaultLocale } from '../../../../shared/models/i18n/i18n'
21import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' 26import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
@@ -24,12 +29,17 @@ import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
24import { getStoredP2PEnabled } from './peertube-player-local-storage' 29import { getStoredP2PEnabled } from './peertube-player-local-storage'
25import { TranslationsManager } from './translations-manager' 30import { TranslationsManager } from './translations-manager'
26 31
32// For VideoJS
33(window as any).WebVTT = require('vtt.js/lib/vtt.js').WebVTT;
34
27// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) 35// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
28videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' 36(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
37
38const CaptionsButton = videojs.getComponent('CaptionsButton') as any
29// Change Captions to Subtitles/CC 39// Change Captions to Subtitles/CC
30videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' 40CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
31// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) 41// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
32videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' 42CaptionsButton.prototype.label_ = ' '
33 43
34export type PlayerMode = 'webtorrent' | 'p2p-media-loader' 44export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
35 45
@@ -92,9 +102,9 @@ export type PeertubePlayerManagerOptions = {
92 102
93export class PeertubePlayerManager { 103export class PeertubePlayerManager {
94 private static playerElementClassName: string 104 private static playerElementClassName: string
95 private static onPlayerChange: (player: any) => void 105 private static onPlayerChange: (player: VideoJsPlayer) => void
96 106
97 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { 107 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: VideoJsPlayer) => void) {
98 let p2pMediaLoader: any 108 let p2pMediaLoader: any
99 109
100 this.onPlayerChange = onPlayerChange 110 this.onPlayerChange = onPlayerChange
@@ -114,12 +124,12 @@ export class PeertubePlayerManager {
114 124
115 const self = this 125 const self = this
116 return new Promise(res => { 126 return new Promise(res => {
117 videojs(options.common.playerElement, videojsOptions, function (this: any) { 127 videojs(options.common.playerElement, videojsOptions, function (this: VideoJsPlayer) {
118 const player = this 128 const player = this
119 129
120 let alreadyFallback = false 130 let alreadyFallback = false
121 131
122 player.tech_.one('error', () => { 132 player.tech(true).one('error', () => {
123 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) 133 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
124 alreadyFallback = true 134 alreadyFallback = true
125 }) 135 })
@@ -164,7 +174,7 @@ export class PeertubePlayerManager {
164 const videojsOptions = this.getVideojsOptions(mode, options) 174 const videojsOptions = this.getVideojsOptions(mode, options)
165 175
166 const self = this 176 const self = this
167 videojs(newVideoElement, videojsOptions, function (this: any) { 177 videojs(newVideoElement, videojsOptions, function (this: VideoJsPlayer) {
168 const player = this 178 const player = this
169 179
170 self.addContextMenu(mode, player, options.common.embedUrl) 180 self.addContextMenu(mode, player, options.common.embedUrl)
@@ -173,7 +183,11 @@ export class PeertubePlayerManager {
173 }) 183 })
174 } 184 }
175 185
176 private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { 186 private static getVideojsOptions (
187 mode: PlayerMode,
188 options: PeertubePlayerManagerOptions,
189 p2pMediaLoaderModule?: any
190 ): VideoJsPlayerOptions {
177 const commonOptions = options.common 191 const commonOptions = options.common
178 192
179 let autoplay = commonOptions.autoplay 193 let autoplay = commonOptions.autoplay
@@ -213,7 +227,7 @@ export class PeertubePlayerManager {
213 html5, 227 html5,
214 228
215 // We don't use text track settings for now 229 // We don't use text track settings for now
216 textTrackSettings: false, 230 textTrackSettings: false as any, // FIXME: typings
217 controls: commonOptions.controls !== undefined ? commonOptions.controls : true, 231 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
218 loop: commonOptions.loop !== undefined ? commonOptions.loop : false, 232 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
219 233
@@ -237,7 +251,7 @@ export class PeertubePlayerManager {
237 peertubeLink: commonOptions.peertubeLink, 251 peertubeLink: commonOptions.peertubeLink,
238 theaterButton: commonOptions.theaterButton, 252 theaterButton: commonOptions.theaterButton,
239 nextVideo: commonOptions.nextVideo 253 nextVideo: commonOptions.nextVideo
240 }) 254 }) as any // FIXME: typings
241 } 255 }
242 } 256 }
243 257
@@ -406,7 +420,7 @@ export class PeertubePlayerManager {
406 return children 420 return children
407 } 421 }
408 422
409 private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { 423 private static addContextMenu (mode: PlayerMode, player: VideoJsPlayer, videoEmbedUrl: string) {
410 const content = [ 424 const content = [
411 { 425 {
412 label: player.localize('Copy the video URL'), 426 label: player.localize('Copy the video URL'),
@@ -416,9 +430,8 @@ export class PeertubePlayerManager {
416 }, 430 },
417 { 431 {
418 label: player.localize('Copy the video URL at the current time'), 432 label: player.localize('Copy the video URL at the current time'),
419 listener: function () { 433 listener: function (this: VideoJsPlayer) {
420 const player = this as videojs.Player 434 copyToClipboard(buildVideoLink({ startTime: this.currentTime() }))
421 copyToClipboard(buildVideoLink({ startTime: player.currentTime() }))
422 } 435 }
423 }, 436 },
424 { 437 {
@@ -432,9 +445,8 @@ export class PeertubePlayerManager {
432 if (mode === 'webtorrent') { 445 if (mode === 'webtorrent') {
433 content.push({ 446 content.push({
434 label: player.localize('Copy magnet URI'), 447 label: player.localize('Copy magnet URI'),
435 listener: function () { 448 listener: function (this: VideoJsPlayer) {
436 const player = this as videojs.Player 449 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
437 copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri)
438 } 450 }
439 }) 451 })
440 } 452 }
@@ -472,7 +484,8 @@ export class PeertubePlayerManager {
472 return event.key === '>' 484 return event.key === '>'
473 }, 485 },
474 handler: function (player: videojs.Player) { 486 handler: function (player: videojs.Player) {
475 player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) 487 const newValue = Math.min(player.playbackRate() + 0.1, 5)
488 player.playbackRate(parseFloat(newValue.toFixed(2)))
476 } 489 }
477 }, 490 },
478 decreasePlaybackRateKey: { 491 decreasePlaybackRateKey: {
@@ -480,7 +493,8 @@ export class PeertubePlayerManager {
480 return event.key === '<' 493 return event.key === '<'
481 }, 494 },
482 handler: function (player: videojs.Player) { 495 handler: function (player: videojs.Player) {
483 player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) 496 const newValue = Math.max(player.playbackRate() - 0.1, 0.10)
497 player.playbackRate(parseFloat(newValue.toFixed(2)))
484 } 498 }
485 }, 499 },
486 frameByFrame: { 500 frameByFrame: {
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 9824c43b5..19d104676 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -1,14 +1,10 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button' 2import './videojs-components/settings-menu-button'
5import { 3import {
6 PeerTubePluginOptions, 4 PeerTubePluginOptions,
7 ResolutionUpdateData, 5 ResolutionUpdateData,
8 UserWatching, 6 UserWatching,
9 VideoJSCaption, 7 VideoJSCaption
10 VideoJSComponentInterface,
11 videojsUntyped
12} from './peertube-videojs-typings' 8} from './peertube-videojs-typings'
13import { isMobile, timeToInt } from './utils' 9import { isMobile, timeToInt } from './utils'
14import { 10import {
@@ -20,7 +16,8 @@ import {
20 saveVolumeInStore 16 saveVolumeInStore
21} from './peertube-player-local-storage' 17} from './peertube-player-local-storage'
22 18
23const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 19const Plugin = videojs.getPlugin('plugin')
20
24class PeerTubePlugin extends Plugin { 21class PeerTubePlugin extends Plugin {
25 private readonly videoViewUrl: string 22 private readonly videoViewUrl: string
26 private readonly videoDuration: number 23 private readonly videoDuration: number
@@ -28,7 +25,6 @@ class PeerTubePlugin extends Plugin {
28 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video 25 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
29 } 26 }
30 27
31 private player: any
32 private videoCaptions: VideoJSCaption[] 28 private videoCaptions: VideoJSCaption[]
33 private defaultSubtitle: string 29 private defaultSubtitle: string
34 30
@@ -40,8 +36,8 @@ class PeerTubePlugin extends Plugin {
40 private mouseInControlBar = false 36 private mouseInControlBar = false
41 private readonly savedInactivityTimeout: number 37 private readonly savedInactivityTimeout: number
42 38
43 constructor (player: videojs.Player, options: PeerTubePluginOptions) { 39 constructor (player: VideoJsPlayer, options?: PeerTubePluginOptions) {
44 super(player, options) 40 super(player)
45 41
46 this.videoViewUrl = options.videoViewUrl 42 this.videoViewUrl = options.videoViewUrl
47 this.videoDuration = options.videoDuration 43 this.videoDuration = options.videoDuration
@@ -67,7 +63,7 @@ class PeerTubePlugin extends Plugin {
67 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) 63 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
68 } 64 }
69 65
70 this.player.tech_.on('loadedqualitydata', () => { 66 this.player.tech(true).on('loadedqualitydata', () => {
71 setTimeout(() => { 67 setTimeout(() => {
72 // Replay a resolution change, now we loaded all quality data 68 // Replay a resolution change, now we loaded all quality data
73 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) 69 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
@@ -102,7 +98,7 @@ class PeerTubePlugin extends Plugin {
102 } 98 }
103 99
104 this.player.textTracks().on('change', () => { 100 this.player.textTracks().on('change', () => {
105 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { 101 const showing = this.player.textTracks().tracks_.find(t => {
106 return t.kind === 'captions' && t.mode === 'showing' 102 return t.kind === 'captions' && t.mode === 'showing'
107 }) 103 })
108 104
@@ -262,7 +258,7 @@ class PeerTubePlugin extends Plugin {
262 258
263 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 259 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
264 private initSmoothProgressBar () { 260 private initSmoothProgressBar () {
265 const SeekBar = videojsUntyped.getComponent('SeekBar') 261 const SeekBar = videojs.getComponent('SeekBar') as any
266 SeekBar.prototype.getPercent = function getPercent () { 262 SeekBar.prototype.getPercent = function getPercent () {
267 // Allows for smooth scrubbing, when player can't keep up. 263 // Allows for smooth scrubbing, when player can't keep up.
268 // const time = (this.player_.scrubbing()) ? 264 // const time = (this.player_.scrubbing()) ?
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index aad4dbb4f..e45722661 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,7 +1,4 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { PeerTubePlugin } from './peertube-plugin' 2import { PeerTubePlugin } from './peertube-plugin'
6import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' 3import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
7import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 4import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
@@ -9,20 +6,45 @@ import { PlayerMode } from './peertube-player-manager'
9import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 6import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
10import { VideoFile } from '@shared/models' 7import { VideoFile } from '@shared/models'
11 8
12declare namespace videojs { 9declare module 'video.js' {
13 interface Player { 10 export interface VideoJsPlayer {
11 theaterEnabled: boolean
12
13 // FIXME: add it to upstream typings
14 posterImage: {
15 show (): void
16 hide (): void
17 }
18
19 handleTechSeeked_ (): void
20
21 // Plugins
22
14 peertube (): PeerTubePlugin 23 peertube (): PeerTubePlugin
15 webtorrent (): WebTorrentPlugin 24 webtorrent (): WebTorrentPlugin
16 p2pMediaLoader (): P2pMediaLoaderPlugin 25 p2pMediaLoader (): P2pMediaLoaderPlugin
17 }
18}
19 26
20interface VideoJSComponentInterface { 27 contextmenuUI (options: any): any
21 _player: videojs.Player 28
29 bezels (): void
30
31 qualityLevels (): { height: number, id: number }[] & {
32 selectedIndex: number
33 selectedIndex_: number
22 34
23 new (player: videojs.Player, options?: any): any 35 addQualityLevel (representation: {
36 id: number
37 label: string
38 height: number
39 _enabled: boolean
40 }): void
41 }
24 42
25 registerComponent (name: string, obj: any): any 43 textTracks (): TextTrackList & {
44 on: Function
45 tracks_: { kind: string, mode: string, language: string }[]
46 }
47 }
26} 48}
27 49
28type VideoJSCaption = { 50type VideoJSCaption = {
@@ -78,9 +100,6 @@ type VideoJSPluginOptions = {
78 p2pMediaLoader?: P2PMediaLoaderPluginOptions 100 p2pMediaLoader?: P2PMediaLoaderPluginOptions
79} 101}
80 102
81// videojs typings don't have some method we need
82const videojsUntyped = videojs as any
83
84type LoadedQualityData = { 103type LoadedQualityData = {
85 qualitySwitchCallback: Function, 104 qualitySwitchCallback: Function,
86 qualityData: { 105 qualityData: {
@@ -123,8 +142,6 @@ export {
123 PlayerNetworkInfo, 142 PlayerNetworkInfo,
124 ResolutionUpdateData, 143 ResolutionUpdateData,
125 AutoResolutionUpdateData, 144 AutoResolutionUpdateData,
126 VideoJSComponentInterface,
127 videojsUntyped,
128 VideoJSCaption, 145 VideoJSCaption,
129 UserWatching, 146 UserWatching,
130 PeerTubePluginOptions, 147 PeerTubePluginOptions,
diff --git a/client/src/assets/player/upnext/end-card.ts b/client/src/assets/player/upnext/end-card.ts
new file mode 100644
index 000000000..d121a83a9
--- /dev/null
+++ b/client/src/assets/player/upnext/end-card.ts
@@ -0,0 +1,155 @@
1import videojs, { VideoJsPlayer } from 'video.js'
2
3function getMainTemplate (options: any) {
4 return `
5 <div class="vjs-upnext-top">
6 <span class="vjs-upnext-headtext">${options.headText}</span>
7 <div class="vjs-upnext-title"></div>
8 </div>
9 <div class="vjs-upnext-autoplay-icon">
10 <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%">
11 <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle>
12 <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle>
13 <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg>
14 </div>
15 <span class="vjs-upnext-bottom">
16 <span class="vjs-upnext-cancel">
17 <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
18 </span>
19 <span class="vjs-upnext-suspended">${options.suspendedText}</span>
20 </span>
21 `
22}
23
24export interface EndCardOptions extends videojs.ComponentOptions {
25 next: Function,
26 getTitle: () => string
27 timeout: number
28 cancelText: string
29 headText: string
30 suspendedText: string
31 condition: () => boolean
32 suspended: () => boolean
33}
34
35const Component = videojs.getComponent('Component')
36class EndCard extends Component {
37 options_: EndCardOptions
38
39 dashOffsetTotal = 586
40 dashOffsetStart = 293
41 interval = 50
42 upNextEvents = new videojs.EventTarget()
43 ticks = 0
44 totalTicks: number
45
46 container: HTMLDivElement
47 title: HTMLElement
48 autoplayRing: HTMLElement
49 cancelButton: HTMLElement
50 suspendedMessage: HTMLElement
51 nextButton: HTMLElement
52
53 constructor (player: VideoJsPlayer, options: EndCardOptions) {
54 super(player, options)
55
56 this.totalTicks = this.options_.timeout / this.interval
57
58 player.on('ended', (_: any) => {
59 if (!this.options_.condition()) return
60
61 player.addClass('vjs-upnext--showing')
62 this.showCard((canceled: boolean) => {
63 player.removeClass('vjs-upnext--showing')
64 this.container.style.display = 'none'
65 if (!canceled) {
66 this.options_.next()
67 }
68 })
69 })
70
71 player.on('playing', () => {
72 this.upNextEvents.trigger('playing')
73 })
74 }
75
76 createEl () {
77 const container = super.createEl('div', {
78 className: 'vjs-upnext-content',
79 innerHTML: getMainTemplate(this.options_)
80 }) as HTMLDivElement
81
82 this.container = container
83 container.style.display = 'none'
84
85 this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] as HTMLElement
86 this.title = container.getElementsByClassName('vjs-upnext-title')[0] as HTMLElement
87 this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] as HTMLElement
88 this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] as HTMLElement
89 this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] as HTMLElement
90
91 this.cancelButton.onclick = () => {
92 this.upNextEvents.trigger('cancel')
93 }
94
95 this.nextButton.onclick = () => {
96 this.upNextEvents.trigger('next')
97 }
98
99 return container
100 }
101
102 showCard (cb: Function) {
103 let timeout: any
104
105 this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart)
106 this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart)
107
108 this.title.innerHTML = this.options_.getTitle()
109
110 this.upNextEvents.one('cancel', () => {
111 clearTimeout(timeout)
112 cb(true)
113 })
114
115 this.upNextEvents.one('playing', () => {
116 clearTimeout(timeout)
117 cb(true)
118 })
119
120 this.upNextEvents.one('next', () => {
121 clearTimeout(timeout)
122 cb(false)
123 })
124
125 const goToPercent = (percent: number) => {
126 const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100)
127 this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
128 }
129
130 const tick = () => {
131 goToPercent((this.ticks++) * 100 / this.totalTicks)
132 }
133
134 const update = () => {
135 if (this.options_.suspended()) {
136 this.suspendedMessage.innerText = this.options_.suspendedText
137 goToPercent(0)
138 this.ticks = 0
139 timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
140 } else if (this.ticks >= this.totalTicks) {
141 clearTimeout(timeout)
142 cb(false)
143 } else {
144 this.suspendedMessage.innerText = ''
145 tick()
146 timeout = setTimeout(update.bind(this), this.interval)
147 }
148 }
149
150 this.container.style.display = 'block'
151 timeout = setTimeout(update.bind(this), this.interval)
152 }
153}
154
155videojs.registerComponent('EndCard', EndCard)
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts
index a3747b25f..6512fec2c 100644
--- a/client/src/assets/player/upnext/upnext-plugin.ts
+++ b/client/src/assets/player/upnext/upnext-plugin.ts
@@ -1,154 +1,11 @@
1// @ts-ignore 1import videojs, { VideoJsPlayer } from 'video.js'
2import * as videojs from 'video.js' 2import { EndCardOptions } from './end-card'
3import { VideoJSComponentInterface } from '../peertube-videojs-typings'
4 3
5function getMainTemplate (options: any) { 4const Plugin = videojs.getPlugin('plugin')
6 return `
7 <div class="vjs-upnext-top">
8 <span class="vjs-upnext-headtext">${options.headText}</span>
9 <div class="vjs-upnext-title"></div>
10 </div>
11 <div class="vjs-upnext-autoplay-icon">
12 <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%">
13 <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle>
14 <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle>
15 <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg>
16 </div>
17 <span class="vjs-upnext-bottom">
18 <span class="vjs-upnext-cancel">
19 <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
20 </span>
21 <span class="vjs-upnext-suspended">${options.suspendedText}</span>
22 </span>
23 `
24}
25
26// @ts-ignore-start
27const Component = videojs.getComponent('Component')
28class EndCard extends Component {
29 options_: any
30 dashOffsetTotal = 586
31 dashOffsetStart = 293
32 interval = 50
33 upNextEvents = new videojs.EventTarget()
34 ticks = 0
35 totalTicks: number
36
37 container: HTMLElement
38 title: HTMLElement
39 autoplayRing: HTMLElement
40 cancelButton: HTMLElement
41 suspendedMessage: HTMLElement
42 nextButton: HTMLElement
43
44 constructor (player: videojs.Player, options: any) {
45 super(player, options)
46
47 this.totalTicks = this.options_.timeout / this.interval
48
49 player.on('ended', (_: any) => {
50 if (!this.options_.condition()) return
51
52 player.addClass('vjs-upnext--showing')
53 this.showCard((canceled: boolean) => {
54 player.removeClass('vjs-upnext--showing')
55 this.container.style.display = 'none'
56 if (!canceled) {
57 this.options_.next()
58 }
59 })
60 })
61
62 player.on('playing', () => {
63 this.upNextEvents.trigger('playing')
64 })
65 }
66
67 createEl () {
68 const container = super.createEl('div', {
69 className: 'vjs-upnext-content',
70 innerHTML: getMainTemplate(this.options_)
71 })
72
73 this.container = container
74 container.style.display = 'none'
75
76 this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0]
77 this.title = container.getElementsByClassName('vjs-upnext-title')[0]
78 this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0]
79 this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0]
80 this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0]
81
82 this.cancelButton.onclick = () => {
83 this.upNextEvents.trigger('cancel')
84 }
85
86 this.nextButton.onclick = () => {
87 this.upNextEvents.trigger('next')
88 }
89
90 return container
91 }
92 5
93 showCard (cb: Function) {
94 let timeout: any
95
96 this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart)
97 this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart)
98
99 this.title.innerHTML = this.options_.getTitle()
100
101 this.upNextEvents.one('cancel', () => {
102 clearTimeout(timeout)
103 cb(true)
104 })
105
106 this.upNextEvents.one('playing', () => {
107 clearTimeout(timeout)
108 cb(true)
109 })
110
111 this.upNextEvents.one('next', () => {
112 clearTimeout(timeout)
113 cb(false)
114 })
115
116 const goToPercent = (percent: number) => {
117 const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100)
118 this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
119 }
120
121 const tick = () => {
122 goToPercent((this.ticks++) * 100 / this.totalTicks)
123 }
124
125 const update = () => {
126 if (this.options_.suspended()) {
127 this.suspendedMessage.innerText = this.options_.suspendedText
128 goToPercent(0)
129 this.ticks = 0
130 timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
131 } else if (this.ticks >= this.totalTicks) {
132 clearTimeout(timeout)
133 cb(false)
134 } else {
135 this.suspendedMessage.innerText = ''
136 tick()
137 timeout = setTimeout(update.bind(this), this.interval)
138 }
139 }
140
141 this.container.style.display = 'block'
142 timeout = setTimeout(update.bind(this), this.interval)
143 }
144}
145// @ts-ignore-end
146
147videojs.registerComponent('EndCard', EndCard)
148
149const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
150class UpNextPlugin extends Plugin { 6class UpNextPlugin extends Plugin {
151 constructor (player: videojs.Player, options: any = {}) { 7
8 constructor (player: VideoJsPlayer, options: Partial<EndCardOptions> = {}) {
152 const settings = { 9 const settings = {
153 next: options.next, 10 next: options.next,
154 getTitle: options.getTitle, 11 getTitle: options.getTitle,
@@ -160,7 +17,7 @@ class UpNextPlugin extends Plugin {
160 suspended: options.suspended 17 suspended: options.suspended
161 } 18 }
162 19
163 super(player, settings) 20 super(player)
164 21
165 this.player.ready(() => { 22 this.player.ready(() => {
166 player.addClass('vjs-upnext') 23 player.addClass('vjs-upnext')
diff --git a/client/src/assets/player/videojs-components/next-video-button.ts b/client/src/assets/player/videojs-components/next-video-button.ts
index bf5c1aba4..bdb245dcc 100644
--- a/client/src/assets/player/videojs-components/next-video-button.ts
+++ b/client/src/assets/player/videojs-components/next-video-button.ts
@@ -1,21 +1,25 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import videojs, { VideoJsPlayer } from 'video.js'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
5 2
6const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 3const Button = videojs.getComponent('Button')
4
5export interface NextVideoButtonOptions extends videojs.ComponentOptions {
6 handler: Function
7}
7 8
8class NextVideoButton extends Button { 9class NextVideoButton extends Button {
10 private readonly nextVideoButtonOptions: NextVideoButtonOptions
9 11
10 constructor (player: Player, options: any) { 12 constructor (player: VideoJsPlayer, options?: NextVideoButtonOptions) {
11 super(player, options) 13 super(player, options)
14
15 this.nextVideoButtonOptions = options
12 } 16 }
13 17
14 createEl () { 18 createEl () {
15 const button = videojsUntyped.dom.createEl('button', { 19 const button = videojs.dom.createEl('button', {
16 className: 'vjs-next-video' 20 className: 'vjs-next-video'
17 }) 21 }) as HTMLButtonElement
18 const nextIcon = videojsUntyped.dom.createEl('span', { 22 const nextIcon = videojs.dom.createEl('span', {
19 className: 'icon icon-next' 23 className: 'icon icon-next'
20 }) 24 })
21 button.appendChild(nextIcon) 25 button.appendChild(nextIcon)
@@ -26,11 +30,8 @@ class NextVideoButton extends Button {
26 } 30 }
27 31
28 handleClick () { 32 handleClick () {
29 this.options_.handler() 33 this.nextVideoButtonOptions.handler()
30 } 34 }
31
32} 35}
33 36
34NextVideoButton.prototype.controlText_ = 'Next video' 37videojs.registerComponent('NextVideoButton', NextVideoButton)
35
36NextVideoButton.registerComponent('NextVideoButton', NextVideoButton)
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
index 6424787b2..db6806fed 100644
--- a/client/src/assets/player/videojs-components/p2p-info-button.ts
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -1,63 +1,64 @@
1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import { PlayerNetworkInfo } from '../peertube-videojs-typings'
2import videojs from 'video.js'
2import { bytes } from '../utils' 3import { bytes } from '../utils'
3 4
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 5const Button = videojs.getComponent('Button')
5class P2pInfoButton extends Button { 6class P2pInfoButton extends Button {
6 7
7 createEl () { 8 createEl () {
8 const div = videojsUntyped.dom.createEl('div', { 9 const div = videojs.dom.createEl('div', {
9 className: 'vjs-peertube' 10 className: 'vjs-peertube'
10 }) 11 })
11 const subDivWebtorrent = videojsUntyped.dom.createEl('div', { 12 const subDivWebtorrent = videojs.dom.createEl('div', {
12 className: 'vjs-peertube-hidden' // Hide the stats before we get the info 13 className: 'vjs-peertube-hidden' // Hide the stats before we get the info
13 }) 14 }) as HTMLDivElement
14 div.appendChild(subDivWebtorrent) 15 div.appendChild(subDivWebtorrent)
15 16
16 const downloadIcon = videojsUntyped.dom.createEl('span', { 17 const downloadIcon = videojs.dom.createEl('span', {
17 className: 'icon icon-download' 18 className: 'icon icon-download'
18 }) 19 })
19 subDivWebtorrent.appendChild(downloadIcon) 20 subDivWebtorrent.appendChild(downloadIcon)
20 21
21 const downloadSpeedText = videojsUntyped.dom.createEl('span', { 22 const downloadSpeedText = videojs.dom.createEl('span', {
22 className: 'download-speed-text' 23 className: 'download-speed-text'
23 }) 24 })
24 const downloadSpeedNumber = videojsUntyped.dom.createEl('span', { 25 const downloadSpeedNumber = videojs.dom.createEl('span', {
25 className: 'download-speed-number' 26 className: 'download-speed-number'
26 }) 27 })
27 const downloadSpeedUnit = videojsUntyped.dom.createEl('span') 28 const downloadSpeedUnit = videojs.dom.createEl('span')
28 downloadSpeedText.appendChild(downloadSpeedNumber) 29 downloadSpeedText.appendChild(downloadSpeedNumber)
29 downloadSpeedText.appendChild(downloadSpeedUnit) 30 downloadSpeedText.appendChild(downloadSpeedUnit)
30 subDivWebtorrent.appendChild(downloadSpeedText) 31 subDivWebtorrent.appendChild(downloadSpeedText)
31 32
32 const uploadIcon = videojsUntyped.dom.createEl('span', { 33 const uploadIcon = videojs.dom.createEl('span', {
33 className: 'icon icon-upload' 34 className: 'icon icon-upload'
34 }) 35 })
35 subDivWebtorrent.appendChild(uploadIcon) 36 subDivWebtorrent.appendChild(uploadIcon)
36 37
37 const uploadSpeedText = videojsUntyped.dom.createEl('span', { 38 const uploadSpeedText = videojs.dom.createEl('span', {
38 className: 'upload-speed-text' 39 className: 'upload-speed-text'
39 }) 40 })
40 const uploadSpeedNumber = videojsUntyped.dom.createEl('span', { 41 const uploadSpeedNumber = videojs.dom.createEl('span', {
41 className: 'upload-speed-number' 42 className: 'upload-speed-number'
42 }) 43 })
43 const uploadSpeedUnit = videojsUntyped.dom.createEl('span') 44 const uploadSpeedUnit = videojs.dom.createEl('span')
44 uploadSpeedText.appendChild(uploadSpeedNumber) 45 uploadSpeedText.appendChild(uploadSpeedNumber)
45 uploadSpeedText.appendChild(uploadSpeedUnit) 46 uploadSpeedText.appendChild(uploadSpeedUnit)
46 subDivWebtorrent.appendChild(uploadSpeedText) 47 subDivWebtorrent.appendChild(uploadSpeedText)
47 48
48 const peersText = videojsUntyped.dom.createEl('span', { 49 const peersText = videojs.dom.createEl('span', {
49 className: 'peers-text' 50 className: 'peers-text'
50 }) 51 })
51 const peersNumber = videojsUntyped.dom.createEl('span', { 52 const peersNumber = videojs.dom.createEl('span', {
52 className: 'peers-number' 53 className: 'peers-number'
53 }) 54 })
54 subDivWebtorrent.appendChild(peersNumber) 55 subDivWebtorrent.appendChild(peersNumber)
55 subDivWebtorrent.appendChild(peersText) 56 subDivWebtorrent.appendChild(peersText)
56 57
57 const subDivHttp = videojsUntyped.dom.createEl('div', { 58 const subDivHttp = videojs.dom.createEl('div', {
58 className: 'vjs-peertube-hidden' 59 className: 'vjs-peertube-hidden'
59 }) 60 })
60 const subDivHttpText = videojsUntyped.dom.createEl('span', { 61 const subDivHttpText = videojs.dom.createEl('span', {
61 className: 'http-fallback', 62 className: 'http-fallback',
62 textContent: 'HTTP' 63 textContent: 'HTTP'
63 }) 64 })
@@ -83,8 +84,8 @@ class P2pInfoButton extends Button {
83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) 84 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
84 const numPeers = p2pStats.numPeers 85 const numPeers = p2pStats.numPeers
85 86
86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + 87 subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
87 this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) 88 this.player().localize('Total uploaded: ' + totalUploaded.join(' '))
88 89
89 downloadSpeedNumber.textContent = downloadSpeed[ 0 ] 90 downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
90 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] 91 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
@@ -92,14 +93,15 @@ class P2pInfoButton extends Button {
92 uploadSpeedNumber.textContent = uploadSpeed[ 0 ] 93 uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] 94 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
94 95
95 peersNumber.textContent = numPeers 96 peersNumber.textContent = numPeers.toString()
96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) 97 peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer'))
97 98
98 subDivHttp.className = 'vjs-peertube-hidden' 99 subDivHttp.className = 'vjs-peertube-hidden'
99 subDivWebtorrent.className = 'vjs-peertube-displayed' 100 subDivWebtorrent.className = 'vjs-peertube-displayed'
100 }) 101 })
101 102
102 return div 103 return div as HTMLButtonElement
103 } 104 }
104} 105}
105Button.registerComponent('P2PInfoButton', P2pInfoButton) 106
107videojs.registerComponent('P2PInfoButton', P2pInfoButton)
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts
index 4d0ea37f5..0db9762a5 100644
--- a/client/src/assets/player/videojs-components/peertube-link-button.ts
+++ b/client/src/assets/player/videojs-components/peertube-link-button.ts
@@ -1,13 +1,10 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from '../utils' 1import { buildVideoLink } from '../utils'
3// FIXME: something weird with our path definition in tsconfig and typings 2import videojs, { VideoJsPlayer } from 'video.js'
4// @ts-ignore
5import { Player } from 'video.js'
6 3
7const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button = videojs.getComponent('Button')
8class PeerTubeLinkButton extends Button { 5class PeerTubeLinkButton extends Button {
9 6
10 constructor (player: Player, options: any) { 7 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
11 super(player, options) 8 super(player, options)
12 } 9 }
13 10
@@ -20,21 +17,22 @@ class PeerTubeLinkButton extends Button {
20 } 17 }
21 18
22 handleClick () { 19 handleClick () {
23 this.player_.pause() 20 this.player().pause()
24 } 21 }
25 22
26 private buildElement () { 23 private buildElement () {
27 const el = videojsUntyped.dom.createEl('a', { 24 const el = videojs.dom.createEl('a', {
28 href: buildVideoLink(), 25 href: buildVideoLink(),
29 innerHTML: 'PeerTube', 26 innerHTML: 'PeerTube',
30 title: this.player_.localize('Go to the video page'), 27 title: this.player().localize('Go to the video page'),
31 className: 'vjs-peertube-link', 28 className: 'vjs-peertube-link',
32 target: '_blank' 29 target: '_blank'
33 }) 30 })
34 31
35 el.addEventListener('mouseenter', () => this.updateHref()) 32 el.addEventListener('mouseenter', () => this.updateHref())
36 33
37 return el 34 return el as HTMLButtonElement
38 } 35 }
39} 36}
40Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) 37
38videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
index b594fc1c5..8168e8f2d 100644
--- a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
+++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
@@ -1,16 +1,12 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import videojs, { VideoJsPlayer } from 'video.js'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
5 2
6const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 3const Component = videojs.getComponent('Component')
7 4
8class PeerTubeLoadProgressBar extends Component { 5class PeerTubeLoadProgressBar extends Component {
9 partEls_: any[]
10 6
11 constructor (player: Player, options: any) { 7 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
12 super(player, options) 8 super(player, options)
13 this.partEls_ = [] 9
14 this.on(player, 'progress', this.update) 10 this.on(player, 'progress', this.update)
15 } 11 }
16 12
@@ -22,8 +18,6 @@ class PeerTubeLoadProgressBar extends Component {
22 } 18 }
23 19
24 dispose () { 20 dispose () {
25 this.partEls_ = null
26
27 super.dispose() 21 super.dispose()
28 } 22 }
29 23
@@ -31,7 +25,8 @@ class PeerTubeLoadProgressBar extends Component {
31 const torrent = this.player().webtorrent().getTorrent() 25 const torrent = this.player().webtorrent().getTorrent()
32 if (!torrent) return 26 if (!torrent) return
33 27
34 this.el_.style.width = (torrent.progress * 100) + '%' 28 // FIXME: typings
29 (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%'
35 } 30 }
36 31
37} 32}
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
index 2de3ece19..0fa6272e7 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-button.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -1,22 +1,19 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore
3import { Player } from 'video.js'
4 2
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 3import { LoadedQualityData } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item' 4import { ResolutionMenuItem } from './resolution-menu-item'
7 5
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 6const Menu = videojs.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') 7const MenuButton = videojs.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton { 8class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement 9 labelEl_: HTMLElement
12 labelEl_: any
13 player: Player
14 10
15 constructor (player: Player, options: any) { 11 constructor (player: VideoJsPlayer, options?: videojs.MenuButtonOptions) {
16 super(player, options) 12 super(player, options)
17 this.player = player
18 13
19 player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) 14 this.controlText('Quality')
15
16 player.tech(true).on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
20 17
21 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) 18 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
22 } 19 }
@@ -24,9 +21,9 @@ class ResolutionMenuButton extends MenuButton {
24 createEl () { 21 createEl () {
25 const el = super.createEl() 22 const el = super.createEl()
26 23
27 this.labelEl_ = videojsUntyped.dom.createEl('div', { 24 this.labelEl_ = videojs.dom.createEl('div', {
28 className: 'vjs-resolution-value' 25 className: 'vjs-resolution-value'
29 }) 26 }) as HTMLElement
30 27
31 el.appendChild(this.labelEl_) 28 el.appendChild(this.labelEl_)
32 29
@@ -55,7 +52,7 @@ class ResolutionMenuButton extends MenuButton {
55 52
56 for (const child of children) { 53 for (const child of children) {
57 if (component !== child) { 54 if (component !== child) {
58 child.selected(false) 55 (child as videojs.MenuItem).selected(false)
59 } 56 }
60 } 57 }
61 }) 58 })
@@ -76,7 +73,7 @@ class ResolutionMenuButton extends MenuButton {
76 if (d.id === -1) continue 73 if (d.id === -1) continue
77 74
78 const label = d.label === '0p' 75 const label = d.label === '0p'
79 ? this.player.localize('Audio-only') 76 ? this.player().localize('Audio-only')
80 : d.label 77 : d.label
81 78
82 this.menu.addChild(new ResolutionMenuItem( 79 this.menu.addChild(new ResolutionMenuItem(
@@ -110,6 +107,5 @@ class ResolutionMenuButton extends MenuButton {
110 this.trigger('menuChanged') 107 this.trigger('menuChanged')
111 } 108 }
112} 109}
113ResolutionMenuButton.prototype.controlText_ = 'Quality'
114 110
115MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) 111videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
index 6c42fefd2..b039c4572 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-item.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -1,12 +1,16 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore 2import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings'
3import { Player } from 'video.js'
4 3
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 4const MenuItem = videojs.getComponent('MenuItem')
5
6export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
7 labels?: { [id: number]: string }
8 id: number
9 callback: Function
10}
6 11
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem { 12class ResolutionMenuItem extends MenuItem {
9 private readonly id: number 13 private readonly resolutionId: number
10 private readonly label: string 14 private readonly label: string
11 // Only used for the automatic item 15 // Only used for the automatic item
12 private readonly labels: { [id: number]: string } 16 private readonly labels: { [id: number]: string }
@@ -15,7 +19,7 @@ class ResolutionMenuItem extends MenuItem {
15 private autoResolutionPossible: boolean 19 private autoResolutionPossible: boolean
16 private currentResolutionLabel: string 20 private currentResolutionLabel: string
17 21
18 constructor (player: Player, options: any) { 22 constructor (player: VideoJsPlayer, options?: ResolutionMenuItemOptions) {
19 options.selectable = true 23 options.selectable = true
20 24
21 super(player, options) 25 super(player, options)
@@ -23,40 +27,40 @@ class ResolutionMenuItem extends MenuItem {
23 this.autoResolutionPossible = true 27 this.autoResolutionPossible = true
24 this.currentResolutionLabel = '' 28 this.currentResolutionLabel = ''
25 29
30 this.resolutionId = options.id
26 this.label = options.label 31 this.label = options.label
27 this.labels = options.labels 32 this.labels = options.labels
28 this.id = options.id
29 this.callback = options.callback 33 this.callback = options.callback
30 34
31 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) 35 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
32 36
33 // We only want to disable the "Auto" item 37 // We only want to disable the "Auto" item
34 if (this.id === -1) { 38 if (this.resolutionId === -1) {
35 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) 39 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
36 } 40 }
37 } 41 }
38 42
39 handleClick (event: any) { 43 handleClick (event: any) {
40 // Auto button disabled? 44 // Auto button disabled?
41 if (this.autoResolutionPossible === false && this.id === -1) return 45 if (this.autoResolutionPossible === false && this.resolutionId === -1) return
42 46
43 super.handleClick(event) 47 super.handleClick(event)
44 48
45 this.callback(this.id, 'video') 49 this.callback(this.resolutionId, 'video')
46 } 50 }
47 51
48 updateSelection (data: ResolutionUpdateData) { 52 updateSelection (data: ResolutionUpdateData) {
49 if (this.id === -1) { 53 if (this.resolutionId === -1) {
50 this.currentResolutionLabel = this.labels[data.id] 54 this.currentResolutionLabel = this.labels[data.id]
51 } 55 }
52 56
53 // Automatic resolution only 57 // Automatic resolution only
54 if (data.auto === true) { 58 if (data.auto === true) {
55 this.selected(this.id === -1) 59 this.selected(this.resolutionId === -1)
56 return 60 return
57 } 61 }
58 62
59 this.selected(this.id === data.id) 63 this.selected(this.resolutionId === data.id)
60 } 64 }
61 65
62 updateAutoResolution (data: AutoResolutionUpdateData) { 66 updateAutoResolution (data: AutoResolutionUpdateData) {
@@ -71,13 +75,13 @@ class ResolutionMenuItem extends MenuItem {
71 } 75 }
72 76
73 getLabel () { 77 getLabel () {
74 if (this.id === -1) { 78 if (this.resolutionId === -1) {
75 return this.label + ' <small>' + this.currentResolutionLabel + '</small>' 79 return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
76 } 80 }
77 81
78 return this.label 82 return this.label
79 } 83 }
80} 84}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) 85videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82 86
83export { ResolutionMenuItem } 87export { ResolutionMenuItem }
diff --git a/client/src/assets/player/videojs-components/settings-dialog.ts b/client/src/assets/player/videojs-components/settings-dialog.ts
new file mode 100644
index 000000000..dd0b1e472
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-dialog.ts
@@ -0,0 +1,37 @@
1import videojs, { VideoJsPlayer } from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsDialog extends Component {
6 constructor (player: VideoJsPlayer) {
7 super(player)
8
9 this.hide()
10 }
11
12 /**
13 * Create the component's DOM element
14 *
15 * @return {Element}
16 * @method createEl
17 */
18 createEl () {
19 const uniqueId = this.id()
20 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
21 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
22
23 return super.createEl('div', {
24 className: 'vjs-settings-dialog vjs-modal-overlay',
25 innerHTML: '',
26 tabIndex: -1
27 }, {
28 'role': 'dialog',
29 'aria-labelledby': dialogLabelId,
30 'aria-describedby': dialogDescriptionId
31 })
32 }
33}
34
35Component.registerComponent('SettingsDialog', SettingsDialog)
36
37export { SettingsDialog }
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index b700f4be6..eae628e7d 100644
--- a/client/src/assets/player/videojs-components/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -1,43 +1,52 @@
1// Author: Yanko Shterev 1// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { SettingsMenuItem } from './settings-menu-item' 2import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from '../utils' 3import { toTitleCase } from '../utils'
4import videojs, { VideoJsPlayer } from 'video.js'
5
6import { SettingsDialog } from './settings-dialog'
7import { SettingsPanel } from './settings-panel'
8import { SettingsPanelChild } from './settings-panel-child'
11 9
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 10const Button = videojs.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 11const Menu = videojs.getComponent('Menu')
14const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const Component = videojs.getComponent('Component')
13
14export interface SettingsButtonOptions extends videojs.ComponentOptions {
15 entries: any[]
16 setup?: {
17 maxHeightOffset: number
18 }
19}
15 20
16class SettingsButton extends Button { 21class SettingsButton extends Button {
17 playerComponent = videojs.Player 22 dialog: SettingsDialog
18 dialog: any 23 dialogEl: HTMLElement
19 dialogEl: any 24 menu: videojs.Menu
20 menu: any 25 panel: SettingsPanel
21 panel: any 26 panelChild: SettingsPanelChild
22 panelChild: any 27
23 28 addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem
24 addSettingsItemHandler: Function 29 disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem
25 disposeSettingsItemHandler: Function 30 playerClickHandler: typeof SettingsButton.prototype.onPlayerClick
26 playerClickHandler: Function 31 userInactiveHandler: typeof SettingsButton.prototype.onUserInactive
27 userInactiveHandler: Function 32
28 33 private settingsButtonOptions: SettingsButtonOptions
29 constructor (player: videojs.Player, options: any) { 34
35 constructor (player: VideoJsPlayer, options?: SettingsButtonOptions) {
30 super(player, options) 36 super(player, options)
31 37
32 this.playerComponent = player 38 this.settingsButtonOptions = options
33 this.dialog = this.playerComponent.addChild('settingsDialog') 39
34 this.dialogEl = this.dialog.el_ 40 this.controlText('Settings')
41
42 this.dialog = this.player().addChild('settingsDialog')
43 this.dialogEl = this.dialog.el() as HTMLElement
35 this.menu = null 44 this.menu = null
36 this.panel = this.dialog.addChild('settingsPanel') 45 this.panel = this.dialog.addChild('settingsPanel')
37 this.panelChild = this.panel.addChild('settingsPanelChild') 46 this.panelChild = this.panel.addChild('settingsPanelChild')
38 47
39 this.addClass('vjs-settings') 48 this.addClass('vjs-settings')
40 this.el_.setAttribute('aria-label', 'Settings Button') 49 this.el().setAttribute('aria-label', 'Settings Button')
41 50
42 // Event handlers 51 // Event handlers
43 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) 52 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
@@ -84,7 +93,7 @@ class SettingsButton extends Button {
84 93
85 this.hideDialog() 94 this.hideDialog()
86 95
87 if (this.options_.entries.length === 0) { 96 if (this.settingsButtonOptions.entries.length === 0) {
88 this.addClass('vjs-hidden') 97 this.addClass('vjs-hidden')
89 } 98 }
90 } 99 }
@@ -103,10 +112,10 @@ class SettingsButton extends Button {
103 } 112 }
104 113
105 bindEvents () { 114 bindEvents () {
106 this.playerComponent.on('click', this.playerClickHandler) 115 this.player().on('click', this.playerClickHandler)
107 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) 116 this.player().on('addsettingsitem', this.addSettingsItemHandler)
108 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) 117 this.player().on('disposesettingsitem', this.disposeSettingsItemHandler)
109 this.playerComponent.on('userinactive', this.userInactiveHandler) 118 this.player().on('userinactive', this.userInactiveHandler)
110 } 119 }
111 120
112 buildCSSClass () { 121 buildCSSClass () {
@@ -122,9 +131,9 @@ class SettingsButton extends Button {
122 } 131 }
123 132
124 showDialog () { 133 showDialog () {
125 this.player_.peertube().onMenuOpen() 134 this.player().peertube().onMenuOpen();
126 135
127 this.menu.el_.style.opacity = '1' 136 (this.menu.el() as HTMLElement).style.opacity = '1'
128 this.dialog.show() 137 this.dialog.show()
129 138
130 this.setDialogSize(this.getComponentSize(this.menu)) 139 this.setDialogSize(this.getComponentSize(this.menu))
@@ -134,23 +143,24 @@ class SettingsButton extends Button {
134 this.player_.peertube().onMenuClosed() 143 this.player_.peertube().onMenuClosed()
135 144
136 this.dialog.hide() 145 this.dialog.hide()
137 this.setDialogSize(this.getComponentSize(this.menu)) 146 this.setDialogSize(this.getComponentSize(this.menu));
138 this.menu.el_.style.opacity = '1' 147 (this.menu.el() as HTMLElement).style.opacity = '1'
139 this.resetChildren() 148 this.resetChildren()
140 } 149 }
141 150
142 getComponentSize (element: any) { 151 getComponentSize (element: videojs.Component | HTMLElement) {
143 let width: number = null 152 let width: number = null
144 let height: number = null 153 let height: number = null
145 154
146 // Could be component or just DOM element 155 // Could be component or just DOM element
147 if (element instanceof Component) { 156 if (element instanceof Component) {
148 width = element.el_.offsetWidth 157 const el = element.el() as HTMLElement
149 height = element.el_.offsetHeight 158
159 width = el.offsetWidth
160 height = el.offsetHeight;
150 161
151 // keep width/height as properties for direct use 162 (element as any).width = width;
152 element.width = width 163 (element as any).height = height
153 element.height = height
154 } else { 164 } else {
155 width = element.offsetWidth 165 width = element.offsetWidth
156 height = element.offsetHeight 166 height = element.offsetHeight
@@ -164,15 +174,17 @@ class SettingsButton extends Button {
164 return 174 return
165 } 175 }
166 176
167 const offset = this.options_.setup.maxHeightOffset 177 const offset = this.settingsButtonOptions.setup.maxHeightOffset
168 const maxHeight = this.playerComponent.el_.offsetHeight - offset 178 const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset // FIXME: typings
179
180 const panelEl = this.panel.el() as HTMLElement
169 181
170 if (height > maxHeight) { 182 if (height > maxHeight) {
171 height = maxHeight 183 height = maxHeight
172 width += 17 184 width += 17
173 this.panel.el_.style.maxHeight = `${height}px` 185 panelEl.style.maxHeight = `${height}px`
174 } else if (this.panel.el_.style.maxHeight !== '') { 186 } else if (panelEl.style.maxHeight !== '') {
175 this.panel.el_.style.maxHeight = '' 187 panelEl.style.maxHeight = ''
176 } 188 }
177 189
178 this.dialogEl.style.width = `${width}px` 190 this.dialogEl.style.width = `${width}px`
@@ -182,7 +194,7 @@ class SettingsButton extends Button {
182 buildMenu () { 194 buildMenu () {
183 this.menu = new Menu(this.player()) 195 this.menu = new Menu(this.player())
184 this.menu.addClass('vjs-main-menu') 196 this.menu.addClass('vjs-main-menu')
185 const entries = this.options_.entries 197 const entries = this.settingsButtonOptions.entries
186 198
187 if (entries.length === 0) { 199 if (entries.length === 0) {
188 this.addClass('vjs-hidden') 200 this.addClass('vjs-hidden')
@@ -191,7 +203,7 @@ class SettingsButton extends Button {
191 } 203 }
192 204
193 for (const entry of entries) { 205 for (const entry of entries) {
194 this.addMenuItem(entry, this.options_) 206 this.addMenuItem(entry, this.settingsButtonOptions)
195 } 207 }
196 208
197 this.panelChild.addChild(this.menu) 209 this.panelChild.addChild(this.menu)
@@ -199,15 +211,17 @@ class SettingsButton extends Button {
199 211
200 addMenuItem (entry: any, options: any) { 212 addMenuItem (entry: any, options: any) {
201 const openSubMenu = function (this: any) { 213 const openSubMenu = function (this: any) {
202 if (videojsUntyped.dom.hasClass(this.el_, 'open')) { 214 if (videojs.dom.hasClass(this.el_, 'open')) {
203 videojsUntyped.dom.removeClass(this.el_, 'open') 215 videojs.dom.removeClass(this.el_, 'open')
204 } else { 216 } else {
205 videojsUntyped.dom.addClass(this.el_, 'open') 217 videojs.dom.addClass(this.el_, 'open')
206 } 218 }
207 } 219 }
208 220
209 options.name = toTitleCase(entry) 221 options.name = toTitleCase(entry)
210 const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) 222
223 const newOptions = Object.assign({}, options, { entry, menuButton: this })
224 const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions)
211 225
212 this.menu.addChild(settingsMenuItem) 226 this.menu.addChild(settingsMenuItem)
213 227
@@ -221,7 +235,7 @@ class SettingsButton extends Button {
221 235
222 resetChildren () { 236 resetChildren () {
223 for (const menuChild of this.menu.children()) { 237 for (const menuChild of this.menu.children()) {
224 menuChild.reset() 238 (menuChild as SettingsMenuItem).reset()
225 } 239 }
226 } 240 }
227 241
@@ -230,75 +244,12 @@ class SettingsButton extends Button {
230 */ 244 */
231 hideChildren () { 245 hideChildren () {
232 for (const menuChild of this.menu.children()) { 246 for (const menuChild of this.menu.children()) {
233 menuChild.hideSubMenu() 247 (menuChild as SettingsMenuItem).hideSubMenu()
234 } 248 }
235 } 249 }
236 250
237} 251}
238 252
239class SettingsPanel extends Component {
240 constructor (player: videojs.Player, options: any) {
241 super(player, options)
242 }
243
244 createEl () {
245 return super.createEl('div', {
246 className: 'vjs-settings-panel',
247 innerHTML: '',
248 tabIndex: -1
249 })
250 }
251}
252
253class SettingsPanelChild extends Component {
254 constructor (player: videojs.Player, options: any) {
255 super(player, options)
256 }
257
258 createEl () {
259 return super.createEl('div', {
260 className: 'vjs-settings-panel-child',
261 innerHTML: '',
262 tabIndex: -1
263 })
264 }
265}
266
267class SettingsDialog extends Component {
268 constructor (player: videojs.Player, options: any) {
269 super(player, options)
270 this.hide()
271 }
272
273 /**
274 * Create the component's DOM element
275 *
276 * @return {Element}
277 * @method createEl
278 */
279 createEl () {
280 const uniqueId = this.id_
281 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
282 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
283
284 return super.createEl('div', {
285 className: 'vjs-settings-dialog vjs-modal-overlay',
286 innerHTML: '',
287 tabIndex: -1
288 }, {
289 'role': 'dialog',
290 'aria-labelledby': dialogLabelId,
291 'aria-describedby': dialogDescriptionId
292 })
293 }
294
295}
296
297SettingsButton.prototype.controlText_ = 'Settings'
298
299Component.registerComponent('SettingsButton', SettingsButton) 253Component.registerComponent('SettingsButton', SettingsButton)
300Component.registerComponent('SettingsDialog', SettingsDialog)
301Component.registerComponent('SettingsPanel', SettingsPanel)
302Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
303 254
304export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } 255export { SettingsButton }
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 84d394c0e..f5671f49d 100644
--- a/client/src/assets/player/videojs-components/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -1,57 +1,63 @@
1// Author: Yanko Shterev 1// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { toTitleCase } from '../utils' 2import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 3import videojs, { VideoJsPlayer } from 'video.js'
10 4import { SettingsButton } from './settings-menu-button'
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 5import { SettingsDialog } from './settings-dialog'
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 6import { SettingsPanel } from './settings-panel'
7import { SettingsPanelChild } from './settings-panel-child'
8
9const MenuItem = videojs.getComponent('MenuItem')
10const component = videojs.getComponent('Component')
11
12export interface SettingsMenuItemOptions extends videojs.MenuItemOptions {
13 entry: string
14 menuButton: SettingsButton
15}
13 16
14class SettingsMenuItem extends MenuItem { 17class SettingsMenuItem extends MenuItem {
15 settingsButton: any 18 settingsButton: SettingsButton
16 dialog: any 19 dialog: SettingsDialog
17 mainMenu: any 20 mainMenu: videojs.Menu
18 panel: any 21 panel: SettingsPanel
19 panelChild: any 22 panelChild: SettingsPanelChild
20 panelChildEl: any 23 panelChildEl: HTMLElement
21 size: any 24 size: number[]
22 menuToLoad: string 25 menuToLoad: string
23 subMenu: any 26 subMenu: SettingsButton
24 27
25 submenuClickHandler: Function 28 submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick
26 transitionEndHandler: Function 29 transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd
27 30
28 settingsSubMenuTitleEl_: any 31 settingsSubMenuTitleEl_: HTMLElement
29 settingsSubMenuValueEl_: any 32 settingsSubMenuValueEl_: HTMLElement
30 settingsSubMenuEl_: any 33 settingsSubMenuEl_: HTMLElement
31 34
32 constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { 35 constructor (player: VideoJsPlayer, options?: SettingsMenuItemOptions) {
33 super(player, options) 36 super(player, options)
34 37
35 this.settingsButton = menuButton 38 this.settingsButton = options.menuButton
36 this.dialog = this.settingsButton.dialog 39 this.dialog = this.settingsButton.dialog
37 this.mainMenu = this.settingsButton.menu 40 this.mainMenu = this.settingsButton.menu
38 this.panel = this.dialog.getChild('settingsPanel') 41 this.panel = this.dialog.getChild('settingsPanel')
39 this.panelChild = this.panel.getChild('settingsPanelChild') 42 this.panelChild = this.panel.getChild('settingsPanelChild')
40 this.panelChildEl = this.panelChild.el_ 43 this.panelChildEl = this.panelChild.el() as HTMLElement
41 44
42 this.size = null 45 this.size = null
43 46
44 // keep state of what menu type is loading next 47 // keep state of what menu type is loading next
45 this.menuToLoad = 'mainmenu' 48 this.menuToLoad = 'mainmenu'
46 49
47 const subMenuName = toTitleCase(entry) 50 const subMenuName = toTitleCase(options.entry)
48 const SubMenuComponent = videojsUntyped.getComponent(subMenuName) 51 const SubMenuComponent = videojs.getComponent(subMenuName)
49 52
50 if (!SubMenuComponent) { 53 if (!SubMenuComponent) {
51 throw new Error(`Component ${subMenuName} does not exist`) 54 throw new Error(`Component ${subMenuName} does not exist`)
52 } 55 }
53 this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) 56
54 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] 57 const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this })
58
59 this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings
60 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[ 0 ]
55 this.settingsSubMenuEl_.className += ' ' + subMenuClass 61 this.settingsSubMenuEl_.className += ' ' + subMenuClass
56 62
57 this.eventHandlers() 63 this.eventHandlers()
@@ -72,7 +78,7 @@ class SettingsMenuItem extends MenuItem {
72 player.on('captionsChanged', () => { 78 player.on('captionsChanged', () => {
73 setTimeout(() => { 79 setTimeout(() => {
74 this.settingsSubMenuEl_.innerHTML = '' 80 this.settingsSubMenuEl_.innerHTML = ''
75 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 81 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
76 this.update() 82 this.update()
77 this.bindClickEvents() 83 this.bindClickEvents()
78 }, 0) 84 }, 0)
@@ -119,27 +125,27 @@ class SettingsMenuItem extends MenuItem {
119 * @method createEl 125 * @method createEl
120 */ 126 */
121 createEl () { 127 createEl () {
122 const el = videojsUntyped.dom.createEl('li', { 128 const el = videojs.dom.createEl('li', {
123 className: 'vjs-menu-item' 129 className: 'vjs-menu-item'
124 }) 130 })
125 131
126 this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { 132 this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', {
127 className: 'vjs-settings-sub-menu-title' 133 className: 'vjs-settings-sub-menu-title'
128 }) 134 }) as HTMLElement
129 135
130 el.appendChild(this.settingsSubMenuTitleEl_) 136 el.appendChild(this.settingsSubMenuTitleEl_)
131 137
132 this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { 138 this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', {
133 className: 'vjs-settings-sub-menu-value' 139 className: 'vjs-settings-sub-menu-value'
134 }) 140 }) as HTMLElement
135 141
136 el.appendChild(this.settingsSubMenuValueEl_) 142 el.appendChild(this.settingsSubMenuValueEl_)
137 143
138 this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { 144 this.settingsSubMenuEl_ = videojs.dom.createEl('div', {
139 className: 'vjs-settings-sub-menu' 145 className: 'vjs-settings-sub-menu'
140 }) 146 }) as HTMLElement
141 147
142 return el 148 return el as HTMLLIElement
143 } 149 }
144 150
145 /** 151 /**
@@ -147,17 +153,17 @@ class SettingsMenuItem extends MenuItem {
147 * 153 *
148 * @method handleClick 154 * @method handleClick
149 */ 155 */
150 handleClick () { 156 handleClick (event: videojs.EventTarget.Event) {
151 this.menuToLoad = 'submenu' 157 this.menuToLoad = 'submenu'
152 // Remove open class to ensure only the open submenu gets this class 158 // Remove open class to ensure only the open submenu gets this class
153 videojsUntyped.dom.removeClass(this.el_, 'open') 159 videojs.dom.removeClass(this.el(), 'open')
154 160
155 super.handleClick() 161 super.handleClick(event);
156 162
157 this.mainMenu.el_.style.opacity = '0' 163 (this.mainMenu.el() as HTMLElement).style.opacity = '0'
158 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element 164 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
159 if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { 165 if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
160 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') 166 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
161 167
162 // animation not played without timeout 168 // animation not played without timeout
163 setTimeout(() => { 169 setTimeout(() => {
@@ -167,7 +173,7 @@ class SettingsMenuItem extends MenuItem {
167 173
168 this.settingsButton.setDialogSize(this.size) 174 this.settingsButton.setDialogSize(this.size)
169 } else { 175 } else {
170 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 176 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
171 } 177 }
172 } 178 }
173 179
@@ -178,9 +184,9 @@ class SettingsMenuItem extends MenuItem {
178 */ 184 */
179 createBackButton () { 185 createBackButton () {
180 const button = this.subMenu.menu.addChild('MenuItem', {}, 0) 186 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
181 button.name_ = 'BackButton' 187
182 button.addClass('vjs-back-button') 188 button.addClass('vjs-back-button');
183 button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) 189 (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
184 } 190 }
185 191
186 /** 192 /**
@@ -189,17 +195,17 @@ class SettingsMenuItem extends MenuItem {
189 * @method PrefixedEvent 195 * @method PrefixedEvent
190 */ 196 */
191 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { 197 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
192 const prefix = ['webkit', 'moz', 'MS', 'o', ''] 198 const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
193 199
194 for (let p = 0; p < prefix.length; p++) { 200 for (let p = 0; p < prefix.length; p++) {
195 if (!prefix[p]) { 201 if (!prefix[ p ]) {
196 type = type.toLowerCase() 202 type = type.toLowerCase()
197 } 203 }
198 204
199 if (action === 'addEvent') { 205 if (action === 'addEvent') {
200 element.addEventListener(prefix[p] + type, callback, false) 206 element.addEventListener(prefix[ p ] + type, callback, false)
201 } else if (action === 'removeEvent') { 207 } else if (action === 'removeEvent') {
202 element.removeEventListener(prefix[p] + type, callback, false) 208 element.removeEventListener(prefix[ p ] + type, callback, false)
203 } 209 }
204 } 210 }
205 } 211 }
@@ -211,7 +217,7 @@ class SettingsMenuItem extends MenuItem {
211 217
212 if (this.menuToLoad === 'mainmenu') { 218 if (this.menuToLoad === 'mainmenu') {
213 // hide submenu 219 // hide submenu
214 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 220 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
215 221
216 // reset opacity to 0 222 // reset opacity to 0
217 this.settingsSubMenuEl_.style.opacity = '0' 223 this.settingsSubMenuEl_.style.opacity = '0'
@@ -219,25 +225,27 @@ class SettingsMenuItem extends MenuItem {
219 } 225 }
220 226
221 reset () { 227 reset () {
222 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 228 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
223 this.settingsSubMenuEl_.style.opacity = '0' 229 this.settingsSubMenuEl_.style.opacity = '0'
224 this.setMargin() 230 this.setMargin()
225 } 231 }
226 232
227 loadMainMenu () { 233 loadMainMenu () {
234 const mainMenuEl = this.mainMenu.el() as HTMLElement
228 this.menuToLoad = 'mainmenu' 235 this.menuToLoad = 'mainmenu'
229 this.mainMenu.show() 236 this.mainMenu.show()
230 this.mainMenu.el_.style.opacity = '0' 237 mainMenuEl.style.opacity = '0'
231 238
232 // back button will always take you to main menu, so set dialog sizes 239 // back button will always take you to main menu, so set dialog sizes
233 this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) 240 const mainMenuAny = this.mainMenu as any
241 this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ])
234 242
235 // animation not triggered without timeout (some async stuff ?!?) 243 // animation not triggered without timeout (some async stuff ?!?)
236 setTimeout(() => { 244 setTimeout(() => {
237 // animate margin and opacity before hiding the submenu 245 // animate margin and opacity before hiding the submenu
238 // this triggers CSS Transition event 246 // this triggers CSS Transition event
239 this.setMargin() 247 this.setMargin()
240 this.mainMenu.el_.style.opacity = '1' 248 mainMenuEl.style.opacity = '1'
241 }, 0) 249 }, 0)
242 } 250 }
243 251
@@ -251,8 +259,8 @@ class SettingsMenuItem extends MenuItem {
251 this.update() 259 this.update()
252 }) 260 })
253 261
254 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) 262 this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText())
255 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 263 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
256 this.panelChildEl.appendChild(this.settingsSubMenuEl_) 264 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
257 this.update() 265 this.update()
258 266
@@ -283,7 +291,8 @@ class SettingsMenuItem extends MenuItem {
283 // or sets options_['selected'] on the selected playback rate. 291 // or sets options_['selected'] on the selected playback rate.
284 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton 292 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
285 if (subMenu === 'PlaybackRateMenuButton') { 293 if (subMenu === 'PlaybackRateMenuButton') {
286 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) 294 const html = (this.subMenu as any).labelEl_.innerHTML
295 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = html, 250)
287 } else { 296 } else {
288 // Loop trough the submenu items to find the selected child 297 // Loop trough the submenu items to find the selected child
289 for (const subMenuItem of this.subMenu.menu.children_) { 298 for (const subMenuItem of this.subMenu.menu.children_) {
@@ -292,13 +301,15 @@ class SettingsMenuItem extends MenuItem {
292 } 301 }
293 302
294 if (subMenuItem.hasClass('vjs-selected')) { 303 if (subMenuItem.hasClass('vjs-selected')) {
304 const subMenuItemUntyped = subMenuItem as any
305
295 // Prefer to use the function 306 // Prefer to use the function
296 if (typeof subMenuItem.getLabel === 'function') { 307 if (typeof subMenuItemUntyped.getLabel === 'function') {
297 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() 308 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel()
298 break 309 break
299 } 310 }
300 311
301 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label 312 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label
302 } 313 }
303 } 314 }
304 } 315 }
@@ -313,7 +324,7 @@ class SettingsMenuItem extends MenuItem {
313 if (!(item instanceof component)) { 324 if (!(item instanceof component)) {
314 continue 325 continue
315 } 326 }
316 item.on(['tap', 'click'], this.submenuClickHandler) 327 item.on([ 'tap', 'click' ], this.submenuClickHandler)
317 } 328 }
318 } 329 }
319 330
@@ -321,11 +332,11 @@ class SettingsMenuItem extends MenuItem {
321 // if number of submenu items change dynamically more logic will be needed 332 // if number of submenu items change dynamically more logic will be needed
322 setSize () { 333 setSize () {
323 this.dialog.removeClass('vjs-hidden') 334 this.dialog.removeClass('vjs-hidden')
324 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') 335 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
325 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) 336 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
326 this.setMargin() 337 this.setMargin()
327 this.dialog.addClass('vjs-hidden') 338 this.dialog.addClass('vjs-hidden')
328 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 339 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
329 } 340 }
330 341
331 setMargin () { 342 setMargin () {
@@ -341,19 +352,19 @@ class SettingsMenuItem extends MenuItem {
341 */ 352 */
342 hideSubMenu () { 353 hideSubMenu () {
343 // after removing settings item this.el_ === null 354 // after removing settings item this.el_ === null
344 if (!this.el_) { 355 if (!this.el()) {
345 return 356 return
346 } 357 }
347 358
348 if (videojsUntyped.dom.hasClass(this.el_, 'open')) { 359 if (videojs.dom.hasClass(this.el(), 'open')) {
349 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 360 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
350 videojsUntyped.dom.removeClass(this.el_, 'open') 361 videojs.dom.removeClass(this.el(), 'open')
351 } 362 }
352 } 363 }
353 364
354} 365}
355 366
356SettingsMenuItem.prototype.contentElType = 'button' 367(SettingsMenuItem as any).prototype.contentElType = 'button'
357videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) 368videojs.registerComponent('SettingsMenuItem', SettingsMenuItem)
358 369
359export { SettingsMenuItem } 370export { SettingsMenuItem }
diff --git a/client/src/assets/player/videojs-components/settings-panel-child.ts b/client/src/assets/player/videojs-components/settings-panel-child.ts
new file mode 100644
index 000000000..d12e8218a
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-panel-child.ts
@@ -0,0 +1,22 @@
1import videojs, { VideoJsPlayer } from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanelChild extends Component {
6
7 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
8 super(player, options)
9 }
10
11 createEl () {
12 return super.createEl('div', {
13 className: 'vjs-settings-panel-child',
14 innerHTML: '',
15 tabIndex: -1
16 })
17 }
18}
19
20Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
21
22export { SettingsPanelChild }
diff --git a/client/src/assets/player/videojs-components/settings-panel.ts b/client/src/assets/player/videojs-components/settings-panel.ts
new file mode 100644
index 000000000..2090abf45
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-panel.ts
@@ -0,0 +1,22 @@
1import videojs, { VideoJsPlayer } from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanel extends Component {
6
7 constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) {
8 super(player, options)
9 }
10
11 createEl () {
12 return super.createEl('div', {
13 className: 'vjs-settings-panel',
14 innerHTML: '',
15 tabIndex: -1
16 })
17 }
18}
19
20Component.registerComponent('SettingsPanel', SettingsPanel)
21
22export { SettingsPanel }
diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts
index bf383cf34..1c8c9f154 100644
--- a/client/src/assets/player/videojs-components/theater-button.ts
+++ b/client/src/assets/player/videojs-components/theater-button.ts
@@ -1,26 +1,24 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' 2import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7 3
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button = videojs.getComponent('Button')
9class TheaterButton extends Button { 5class TheaterButton extends Button {
10 6
11 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' 7 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled'
12 8
13 constructor (player: videojs.Player, options: any) { 9 constructor (player: VideoJsPlayer, options: videojs.ComponentOptions) {
14 super(player, options) 10 super(player, options)
15 11
16 const enabled = getStoredTheater() 12 const enabled = getStoredTheater()
17 if (enabled === true) { 13 if (enabled === true) {
18 this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) 14 this.player().addClass(TheaterButton.THEATER_MODE_CLASS)
19 15
20 this.handleTheaterChange() 16 this.handleTheaterChange()
21 } 17 }
22 18
23 this.player_.theaterEnabled = enabled 19 this.controlText('Theater mode')
20
21 this.player().theaterEnabled = enabled
24 } 22 }
25 23
26 buildCSSClass () { 24 buildCSSClass () {
@@ -52,6 +50,4 @@ class TheaterButton extends Button {
52 } 50 }
53} 51}
54 52
55TheaterButton.prototype.controlText_ = 'Theater mode' 53videojs.registerComponent('TheaterButton', TheaterButton)
56
57TheaterButton.registerComponent('TheaterButton', TheaterButton)
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 35cf85c99..d26fc38fa 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -1,17 +1,15 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs, { VideoJsPlayer } from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4 2
5import * as WebTorrent from 'webtorrent' 3import * as WebTorrent from 'webtorrent'
6import { renderVideo } from './video-renderer' 4import { renderVideo } from './video-renderer'
7import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' 5import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings'
8import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' 6import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
9import { PeertubeChunkStore } from './peertube-chunk-store' 7import { PeertubeChunkStore } from './peertube-chunk-store'
10import { 8import {
11 getAverageBandwidthInStore, 9 getAverageBandwidthInStore,
12 getStoredMute, 10 getStoredMute,
13 getStoredVolume,
14 getStoredP2PEnabled, 11 getStoredP2PEnabled,
12 getStoredVolume,
15 saveAverageBandwidth 13 saveAverageBandwidth
16} from '../peertube-player-local-storage' 14} from '../peertube-player-local-storage'
17import { VideoFile } from '@shared/models' 15import { VideoFile } from '@shared/models'
@@ -24,13 +22,14 @@ type PlayOptions = {
24 delay?: number 22 delay?: number
25} 23}
26 24
27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 25const Plugin = videojs.getPlugin('plugin')
26
28class WebTorrentPlugin extends Plugin { 27class WebTorrentPlugin extends Plugin {
29 private readonly playerElement: HTMLVideoElement 28 private readonly playerElement: HTMLVideoElement
30 29
31 private readonly autoplay: boolean = false 30 private readonly autoplay: boolean = false
32 private readonly startTime: number = 0 31 private readonly startTime: number = 0
33 private readonly savePlayerSrcFunction: Function 32 private readonly savePlayerSrcFunction: VideoJsPlayer['src']
34 private readonly videoFiles: VideoFile[] 33 private readonly videoFiles: VideoFile[]
35 private readonly videoDuration: number 34 private readonly videoDuration: number
36 private readonly CONSTANTS = { 35 private readonly CONSTANTS = {
@@ -49,7 +48,6 @@ class WebTorrentPlugin extends Plugin {
49 dht: false 48 dht: false
50 }) 49 })
51 50
52 private player: any
53 private currentVideoFile: VideoFile 51 private currentVideoFile: VideoFile
54 private torrent: WebTorrent.Torrent 52 private torrent: WebTorrent.Torrent
55 53
@@ -70,8 +68,8 @@ class WebTorrentPlugin extends Plugin {
70 68
71 private downloadSpeeds: number[] = [] 69 private downloadSpeeds: number[] = []
72 70
73 constructor (player: videojs.Player, options: WebtorrentPluginOptions) { 71 constructor (player: VideoJsPlayer, options?: WebtorrentPluginOptions) {
74 super(player, options) 72 super(player)
75 73
76 this.startTime = timeToInt(options.startTime) 74 this.startTime = timeToInt(options.startTime)
77 75
@@ -147,12 +145,12 @@ class WebTorrentPlugin extends Plugin {
147 } 145 }
148 146
149 // Do not display error to user because we will have multiple fallback 147 // Do not display error to user because we will have multiple fallback
150 this.disableErrorDisplay() 148 this.disableErrorDisplay();
151 149
152 // Hack to "simulate" src link in video.js >= 6 150 // Hack to "simulate" src link in video.js >= 6
153 // Without this, we can't play the video after pausing it 151 // Without this, we can't play the video after pausing it
154 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 152 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
155 this.player.src = () => true 153 (this.player as any).src = () => true
156 const oldPlaybackRate = this.player.playbackRate() 154 const oldPlaybackRate = this.player.playbackRate()
157 155
158 const previousVideoFile = this.currentVideoFile 156 const previousVideoFile = this.currentVideoFile
@@ -333,7 +331,7 @@ class WebTorrentPlugin extends Plugin {
333 331
334 const playPromise = this.player.play() 332 const playPromise = this.player.play()
335 if (playPromise !== undefined) { 333 if (playPromise !== undefined) {
336 return playPromise.then(done) 334 return playPromise.then(() => done())
337 .catch((err: Error) => { 335 .catch((err: Error) => {
338 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { 336 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) {
339 return 337 return
@@ -426,8 +424,8 @@ class WebTorrentPlugin extends Plugin {
426 } 424 }
427 425
428 // Proxy first play 426 // Proxy first play
429 const oldPlay = this.player.play.bind(this.player) 427 const oldPlay = this.player.play.bind(this.player);
430 this.player.play = () => { 428 (this.player as any).play = () => {
431 this.player.addClass('vjs-has-big-play-button-clicked') 429 this.player.addClass('vjs-has-big-play-button-clicked')
432 this.player.play = oldPlay 430 this.player.play = oldPlay
433 431
@@ -619,7 +617,7 @@ class WebTorrentPlugin extends Plugin {
619 video: qualityLevelsPayload 617 video: qualityLevelsPayload
620 } 618 }
621 } 619 }
622 this.player.tech_.trigger('loadedqualitydata', payload) 620 this.player.tech(true).trigger('loadedqualitydata', payload)
623 } 621 }
624 622
625 private buildQualityLabel (file: VideoFile) { 623 private buildQualityLabel (file: VideoFile) {
@@ -651,9 +649,9 @@ class WebTorrentPlugin extends Plugin {
651 return 649 return
652 } 650 }
653 651
654 for (let i = 0; i < qualityLevels; i++) { 652 for (let i = 0; i < qualityLevels.length; i++) {
655 const q = this.player.qualityLevels[i] 653 const q = qualityLevels[i]
656 if (q.height === resolutionId) qualityLevels.selectedIndex = i 654 if (q.height === resolutionId) qualityLevels.selectedIndex_ = i
657 } 655 }
658 } 656 }
659} 657}