aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/package.json7
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts49
-rw-r--r--client/src/app/+about/about-instance/about-instance.resolver.ts27
-rw-r--r--client/src/app/+about/about-routing.module.ts4
-rw-r--r--client/src/app/+about/about.module.ts2
-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/config/edit-custom-config/edit-custom-config.component.html8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss5
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.scss2
-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/+my-account/my-account-video-channels/my-account-video-channel-edit.ts2
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html2
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html2
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/search/search-filters.component.html6
-rw-r--r--client/src/app/shared/buttons/button.component.scss23
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts2
-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/menu/top-menu-dropdown.component.ts3
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts4
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts4
-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.html2
-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/hls-plugin.ts626
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts36
-rw-r--r--client/src/assets/player/peertube-player-manager.ts72
-rw-r--r--client/src/assets/player/peertube-plugin.ts22
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts82
-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.ts39
-rw-r--r--client/src/sass/application.scss24
-rw-r--r--client/src/sass/include/_mixins.scss13
-rw-r--r--client/src/sass/include/_variables.scss2
-rw-r--r--client/src/sass/player/_player-variables.scss2
-rw-r--r--client/src/sass/player/bezels.scss8
-rw-r--r--client/src/sass/player/peertube-skin.scss9
-rw-r--r--client/src/standalone/videos/embed.ts40
-rw-r--r--client/tsconfig.json2
-rw-r--r--client/webpack/webpack.video-embed.js2
-rw-r--r--client/yarn.lock68
75 files changed, 2059 insertions, 819 deletions
diff --git a/client/package.json b/client/package.json
index cd0a82aa4..66e8990f9 100644
--- a/client/package.json
+++ b/client/package.json
@@ -56,7 +56,6 @@
56 "@ngx-loading-bar/router": "^4.2.0", 56 "@ngx-loading-bar/router": "^4.2.0",
57 "@ngx-meta/core": "^7.0.0", 57 "@ngx-meta/core": "^7.0.0",
58 "@ngx-translate/i18n-polyfill": "^1.0.0", 58 "@ngx-translate/i18n-polyfill": "^1.0.0",
59 "@streamroot/videojs-hlsjs-plugin": "^1.0.10",
60 "@types/core-js": "^2.5.2", 59 "@types/core-js": "^2.5.2",
61 "@types/debug": "^4.1.5", 60 "@types/debug": "^4.1.5",
62 "@types/hls.js": "^0.12.4", 61 "@types/hls.js": "^0.12.4",
@@ -65,11 +64,11 @@
65 "@types/jschannel": "^1.0.0", 64 "@types/jschannel": "^1.0.0",
66 "@types/linkifyjs": "^2.1.2", 65 "@types/linkifyjs": "^2.1.2",
67 "@types/lodash-es": "^4.17.0", 66 "@types/lodash-es": "^4.17.0",
68 "@types/markdown-it": "^0.0.5", 67 "@types/markdown-it": "^0.0.9",
69 "@types/node": "^10.9.2", 68 "@types/node": "^10.9.2",
70 "@types/sanitize-html": "1.18.0", 69 "@types/sanitize-html": "1.18.0",
71 "@types/socket.io-client": "^1.4.32", 70 "@types/socket.io-client": "^1.4.32",
72 "@types/video.js": "^7.2.5", 71 "@types/video.js": "^7.3.3",
73 "@types/webtorrent": "^0.107.0", 72 "@types/webtorrent": "^0.107.0",
74 "angular2-hotkeys": "^2.1.2", 73 "angular2-hotkeys": "^2.1.2",
75 "angularx-qrcode": "1.6.4", 74 "angularx-qrcode": "1.6.4",
@@ -77,6 +76,7 @@
77 "bootstrap": "^4.1.3", 76 "bootstrap": "^4.1.3",
78 "buffer": "^5.1.0", 77 "buffer": "^5.1.0",
79 "cache-chunk-store": "^3.0.0", 78 "cache-chunk-store": "^3.0.0",
79 "chart.js": "^2.9.3",
80 "codelyzer": "^5.0.1", 80 "codelyzer": "^5.0.1",
81 "core-js": "^3.1.4", 81 "core-js": "^3.1.4",
82 "css-loader": "^3.1.0", 82 "css-loader": "^3.1.0",
@@ -132,6 +132,7 @@
132 "videojs-dock": "^2.0.2", 132 "videojs-dock": "^2.0.2",
133 "videojs-hotkeys": "^0.2.21", 133 "videojs-hotkeys": "^0.2.21",
134 "videostream": "~3.2.1", 134 "videostream": "~3.2.1",
135 "vtt.js": "^0.13.0",
135 "webpack-bundle-analyzer": "^3.0.2", 136 "webpack-bundle-analyzer": "^3.0.2",
136 "webpack-cli": "^3.0.8", 137 "webpack-cli": "^3.0.8",
137 "webtorrent": "^0.107.16", 138 "webtorrent": "^0.107.16",
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts
index 87beb13da..c8c156105 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -1,12 +1,10 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 3import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
5import { InstanceService } from '@app/shared/instance/instance.service' 4import { InstanceService } from '@app/shared/instance/instance.service'
6import { MarkdownService } from '@app/shared/renderer'
7import { forkJoin } from 'rxjs'
8import { map, switchMap } from 'rxjs/operators'
9import { ServerConfig } from '@shared/models' 5import { ServerConfig } from '@shared/models'
6import { ActivatedRoute } from '@angular/router'
7import { ResolverData } from './about-instance.resolver'
10 8
11@Component({ 9@Component({
12 selector: 'my-about-instance', 10 selector: 'my-about-instance',
@@ -37,11 +35,10 @@ export class AboutInstanceComponent implements OnInit {
37 serverConfig: ServerConfig 35 serverConfig: ServerConfig
38 36
39 constructor ( 37 constructor (
38 private route: ActivatedRoute,
40 private notifier: Notifier, 39 private notifier: Notifier,
41 private serverService: ServerService, 40 private serverService: ServerService,
42 private instanceService: InstanceService, 41 private instanceService: InstanceService
43 private markdownService: MarkdownService,
44 private i18n: I18n
45 ) {} 42 ) {}
46 43
47 get instanceName () { 44 get instanceName () {
@@ -56,35 +53,23 @@ export class AboutInstanceComponent implements OnInit {
56 return this.serverConfig.instance.isNSFW 53 return this.serverConfig.instance.isNSFW
57 } 54 }
58 55
59 ngOnInit () { 56 async ngOnInit () {
60 this.serverConfig = this.serverService.getTmpConfig() 57 this.serverConfig = this.serverService.getTmpConfig()
61 this.serverService.getConfig() 58 this.serverService.getConfig()
62 .subscribe(config => this.serverConfig = config) 59 .subscribe(config => this.serverConfig = config)
63 60
64 this.instanceService.getAbout() 61 const { about, languages, categories }: ResolverData = this.route.snapshot.data.instanceData
65 .pipe( 62
66 switchMap(about => { 63 this.languages = languages
67 return forkJoin([ 64 this.categories = categories
68 this.instanceService.buildTranslatedLanguages(about), 65
69 this.instanceService.buildTranslatedCategories(about) 66 this.shortDescription = about.instance.shortDescription
70 ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }))) 67
71 }) 68 this.creationReason = about.instance.creationReason
72 ).subscribe( 69 this.maintenanceLifetime = about.instance.maintenanceLifetime
73 async ({ about, languages, categories }) => { 70 this.businessModel = about.instance.businessModel
74 this.languages = languages 71
75 this.categories = categories 72 this.html = await this.instanceService.buildHtml(about)
76
77 this.shortDescription = about.instance.shortDescription
78
79 this.creationReason = about.instance.creationReason
80 this.maintenanceLifetime = about.instance.maintenanceLifetime
81 this.businessModel = about.instance.businessModel
82
83 this.html = await this.instanceService.buildHtml(about)
84 },
85
86 () => this.notifier.error(this.i18n('Cannot get about information from server'))
87 )
88 } 73 }
89 74
90 openContactModal () { 75 openContactModal () {
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts
new file mode 100644
index 000000000..94c6abe5a
--- /dev/null
+++ b/client/src/app/+about/about-instance/about-instance.resolver.ts
@@ -0,0 +1,27 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
3import { map, switchMap } from 'rxjs/operators'
4import { forkJoin } from 'rxjs'
5import { InstanceService } from '@app/shared/instance/instance.service'
6import { About } from '@shared/models/server'
7
8export type ResolverData = { about: About, languages: string[], categories: string[] }
9
10@Injectable()
11export class AboutInstanceResolver implements Resolve<any> {
12 constructor (
13 private instanceService: InstanceService
14 ) {}
15
16 resolve (route: ActivatedRouteSnapshot) {
17 return this.instanceService.getAbout()
18 .pipe(
19 switchMap(about => {
20 return forkJoin([
21 this.instanceService.buildTranslatedLanguages(about),
22 this.instanceService.buildTranslatedCategories(about)
23 ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories })))
24 })
25 )
26 }
27}
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts
index 33e5070cb..91ccb846f 100644
--- a/client/src/app/+about/about-routing.module.ts
+++ b/client/src/app/+about/about-routing.module.ts
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' 5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
8import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
8 9
9const aboutRoutes: Routes = [ 10const aboutRoutes: Routes = [
10 { 11 {
@@ -24,6 +25,9 @@ const aboutRoutes: Routes = [
24 meta: { 25 meta: {
25 title: 'About this instance' 26 title: 'About this instance'
26 } 27 }
28 },
29 resolve: {
30 instanceData: AboutInstanceResolver
27 } 31 }
28 }, 32 },
29 { 33 {
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index 14bf76e55..84d697540 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -7,6 +7,7 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
7import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 7import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
8import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 8import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
9import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component' 9import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component'
10import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
10 11
11@NgModule({ 12@NgModule({
12 imports: [ 13 imports: [
@@ -28,6 +29,7 @@ import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/a
28 ], 29 ],
29 30
30 providers: [ 31 providers: [
32 AboutInstanceResolver
31 ] 33 ]
32}) 34})
33export class AboutModule { } 35export class AboutModule { }
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/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 915d60090..d806ea355 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -234,6 +234,9 @@
234 inputName="signupEnabled" formControlName="enabled" 234 inputName="signupEnabled" formControlName="enabled"
235 i18n-labelText labelText="Signup enabled" 235 i18n-labelText labelText="Signup enabled"
236 > 236 >
237 <ng-container ngProjectAs="description">
238 <span i18n>⚠️ This functionality requires a lot of attention and extra moderation.</span>
239 </ng-container>
237 <ng-container ngProjectAs="extra"> 240 <ng-container ngProjectAs="extra">
238 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" 241 <my-peertube-checkbox [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }"
239 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" 242 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
@@ -243,10 +246,11 @@
243 <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3"> 246 <div [ngClass]="{ 'disabled-checkbox-extra': !isSignupEnabled() }" class="mt-3">
244 <label i18n for="signupLimit">Signup limit</label> 247 <label i18n for="signupLimit">Signup limit</label>
245 <input 248 <input
246 type="text" id="signupLimit" 249 type="number" min="-1" id="signupLimit"
247 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }" 250 formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
248 > 251 >
249 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div> 252 <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
253 <small *ngIf="form.value['signup']['limit'] === -1" class="text-muted">Signup won't be limited to a fixed number of users.</small>
250 </div> 254 </div>
251 </ng-container> 255 </ng-container>
252 </my-peertube-checkbox> 256 </my-peertube-checkbox>
@@ -318,7 +322,7 @@
318 i18n-labelText labelText="Blacklist new videos automatically" 322 i18n-labelText labelText="Blacklist new videos automatically"
319 > 323 >
320 <ng-container ngProjectAs="description"> 324 <ng-container ngProjectAs="description">
321 <span i18n>Videos of regular users will stay private until a moderator reviews them. Can be overriden per user.</span> 325 <span i18n>Unless a user is marked as trusted, their videos will stay private until a moderator reviews them.</span>
322 </ng-container> 326 </ng-container>
323 </my-peertube-checkbox> 327 </my-peertube-checkbox>
324 </div> 328 </div>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 60d608028..dd70f1c06 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -10,6 +10,11 @@ input[type=text] {
10 display: block; 10 display: block;
11} 11}
12 12
13input[type=number] {
14 @include peertube-input-text(315px);
15 display: block;
16}
17
13input[type=checkbox] { 18input[type=checkbox] {
14 @include peertube-checkbox(1px); 19 @include peertube-checkbox(1px);
15} 20}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss
index 1baddc95f..df104c14e 100644
--- a/client/src/app/+admin/follows/following-add/following-add.component.scss
+++ b/client/src/app/+admin/follows/following-add/following-add.component.scss
@@ -7,7 +7,7 @@ textarea {
7 7
8.form-control { 8.form-control {
9 &, &:focus { 9 &, &:focus {
10 background-color: var(--inputColor); 10 background-color: var(--inputBackgroundColor);
11 color: var(--mainForegroundColor); 11 color: var(--mainForegroundColor);
12 } 12 }
13} 13}
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/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
index 7479442d1..355cb4f55 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts
@@ -9,7 +9,7 @@ export abstract class MyAccountVideoChannelEdit extends FormReactive {
9 abstract isCreation (): boolean 9 abstract isCreation (): boolean
10 abstract getFormButtonTitle (): string 10 abstract getFormButtonTitle (): string
11 11
12 // FIXME: We need this method so angular does not complain in the child template 12 // We need this method so angular does not complain in child template that doesn't need this
13 onAvatarChange (formData: FormData) { /* empty */ } 13 onAvatarChange (formData: FormData) { /* empty */ }
14 14
15 // Should be implemented by the child 15 // Should be implemented by the child
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html
index 88ff6e3ff..170c2964e 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.html
+++ b/client/src/app/+signup/+register/register-step-channel.component.html
@@ -40,7 +40,7 @@
40 </div> 40 </div>
41 41
42 <div class="name-information" i18n> 42 <div class="name-information" i18n>
43 The channel name is a unique identifier of your channel on this instance. It's like an address mail, so other people can find your channel. 43 The channel name is a unique identifier of your channel on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
44 </div> 44 </div>
45 45
46 <div *ngIf="formErrors.name" class="form-error"> 46 <div *ngIf="formErrors.name" class="form-error">
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
index a2a657660..6bac4e4a4 100644
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -29,7 +29,7 @@
29 </div> 29 </div>
30 30
31 <div class="name-information" i18n> 31 <div class="name-information" i18n>
32 The username is a unique identifier of your account on this instance. It's like an address mail, so other people can find you. 32 The username is a unique identifier of your account on this and all the other instances. It's as unique as an email address, which makes it easy for other people to interact with it.
33 </div> 33 </div>
34 34
35 <div *ngIf="formErrors.username" class="form-error"> 35 <div *ngIf="formErrors.username" class="form-error">
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index dda705811..62915ec54 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -48,8 +48,6 @@ export function metaFactory (serverService: ServerService): MetaLoader {
48 ], 48 ],
49 imports: [ 49 imports: [
50 BrowserModule, 50 BrowserModule,
51 // FIXME: https://github.com/maxisam/ngx-clipboard/issues/133
52 ClipboardModule,
53 51
54 CoreModule, 52 CoreModule,
55 SharedModule, 53 SharedModule,
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/search/search-filters.component.html b/client/src/app/search/search-filters.component.html
index 07fb2c048..c275285d5 100644
--- a/client/src/app/search/search-filters.component.html
+++ b/client/src/app/search/search-filters.component.html
@@ -103,7 +103,7 @@
103 </button> 103 </button>
104 <div class="peertube-select-container"> 104 <div class="peertube-select-container">
105 <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf"> 105 <select id="category" name="category" [(ngModel)]="advancedSearch.categoryOneOf">
106 <option [value]="undefined" i18n>Any or no category set</option> 106 <option [value]="undefined" i18n>Display all categories</option>
107 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> 107 <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
108 </select> 108 </select>
109 </div> 109 </div>
@@ -116,7 +116,7 @@
116 </button> 116 </button>
117 <div class="peertube-select-container"> 117 <div class="peertube-select-container">
118 <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf"> 118 <select id="licence" name="licence" [(ngModel)]="advancedSearch.licenceOneOf">
119 <option [value]="undefined" i18n>Any or no license set</option> 119 <option [value]="undefined" i18n>Display all licenses</option>
120 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> 120 <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
121 </select> 121 </select>
122 </div> 122 </div>
@@ -129,7 +129,7 @@
129 </button> 129 </button>
130 <div class="peertube-select-container"> 130 <div class="peertube-select-container">
131 <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf"> 131 <select id="language" name="language" [(ngModel)]="advancedSearch.languageOneOf">
132 <option [value]="undefined" i18n>Any or no language set</option> 132 <option [value]="undefined" i18n>Display all languages</option>
133 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> 133 <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
134 </select> 134 </select>
135 </div> 135 </div>
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss
index 2a8cfc748..3ccfefd7e 100644
--- a/client/src/app/shared/buttons/button.component.scss
+++ b/client/src/app/shared/buttons/button.component.scss
@@ -10,11 +10,26 @@ my-small-loader ::ng-deep .root {
10.action-button { 10.action-button {
11 @include peertube-button-link; 11 @include peertube-button-link;
12 @include button-with-icon(21px, 0, -2px); 12 @include button-with-icon(21px, 0, -2px);
13}
13 14
14 // FIXME: Firefox does not apply global .orange-button icon color 15.orange-button {
15 &.orange-button { 16 @include peertube-button;
16 @include apply-svg-color(#fff) 17 @include orange-button;
17 } 18}
19
20.orange-button-link {
21 @include peertube-button-link;
22 @include orange-button;
23}
24
25.grey-button {
26 @include peertube-button;
27 @include grey-button;
28}
29
30.grey-button-link {
31 @include peertube-button-link;
32 @include grey-button;
18} 33}
19 34
20// In a table, try to minimize the space taken by this button 35// In a table, try to minimize the space taken by this button
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 767e3f026..d20754d11 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -56,7 +56,7 @@ export class CustomConfigValidatorsService {
56 } 56 }
57 57
58 this.SIGNUP_LIMIT = { 58 this.SIGNUP_LIMIT = {
59 VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ], 59 VALIDATORS: [ Validators.required, Validators.min(-1), Validators.pattern('-?[0-9]+') ],
60 MESSAGES: { 60 MESSAGES: {
61 'required': this.i18n('Signup limit is required.'), 61 'required': this.i18n('Signup limit is required.'),
62 'min': this.i18n('Signup limit must be greater than 1.'), 62 'min': this.i18n('Signup limit must be greater than 1.'),
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/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
index 5ccdafb54..24a083654 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.ts
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -49,8 +49,7 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
49 e => e.children && e.children.some(c => !!c.iconName) 49 e => e.children && e.children.some(c => !!c.iconName)
50 ) 50 )
51 51
52 // FIXME: We have to set body for the container to avoid because of scroll overflow on mobile view 52 // We have to set body for the container to avoid scroll overflow on mobile view
53 // But this break our hovering system
54 if (this.screen.isInMobileView()) { 53 if (this.screen.isInMobileView()) {
55 this.container = 'body' 54 this.container = 'body'
56 } 55 }
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 0d3fde537..f0c87326f 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { MarkdownIt } from 'markdown-it'
3import { buildVideoLink } from '../../../assets/player/utils' 2import { buildVideoLink } from '../../../assets/player/utils'
4import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service' 3import { HtmlRendererService } from '@app/shared/renderer/html-renderer.service'
4import * as MarkdownIt from 'markdown-it'
5 5
6type MarkdownParsers = { 6type MarkdownParsers = {
7 textMarkdownIt: MarkdownIt 7 textMarkdownIt: MarkdownIt
@@ -100,7 +100,7 @@ export class MarkdownService {
100 } 100 }
101 101
102 private async createMarkdownIt (config: MarkdownConfig) { 102 private async createMarkdownIt (config: MarkdownConfig) {
103 // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 103 // FIXME: import('...') returns a struct module, containing a "default" field
104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 104 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
105 105
106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) 106 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html })
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/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts
index 9f613c5fa..f09c3d1fc 100644
--- a/client/src/app/shared/video/infinite-scroller.directive.ts
+++ b/client/src/app/shared/video/infinite-scroller.directive.ts
@@ -1,4 +1,4 @@
1import { distinctUntilChanged, filter, map, share, startWith, tap, throttleTime } from 'rxjs/operators' 1import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators'
2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' 2import { AfterContentChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'
3import { fromEvent, Observable, Subscription } from 'rxjs' 3import { fromEvent, Observable, Subscription } from 'rxjs'
4 4
@@ -53,7 +53,7 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterConten
53 const scrollableElement = this.onItself ? this.container : window 53 const scrollableElement = this.onItself ? this.container : window
54 const scrollObservable = fromEvent(scrollableElement, 'scroll') 54 const scrollObservable = fromEvent(scrollableElement, 'scroll')
55 .pipe( 55 .pipe(
56 startWith(null as string), // FIXME: typings 56 startWith(true),
57 throttleTime(200, undefined, throttleOptions), 57 throttleTime(200, undefined, throttleOptions),
58 map(() => this.getScrollInfo()), 58 map(() => this.getScrollInfo()),
59 distinctUntilChanged((o1, o2) => o1.current === o2.current), 59 distinctUntilChanged((o1, o2) => o1.current === o2.current),
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.html b/client/src/app/shared/video/video-miniature.component.html
index 46c49c15b..819be6d48 100644
--- a/client/src/app/shared/video/video-miniature.component.html
+++ b/client/src/app/shared/video/video-miniature.component.html
@@ -50,7 +50,7 @@
50 </div> 50 </div>
51 51
52 <div class="video-actions"> 52 <div class="video-actions">
53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown --> 53 <!-- FIXME: remove bottom placement when overflow is fixed in bootstrap dropdown: https://github.com/ng-bootstrap/ng-bootstrap/issues/3495 -->
54 <my-video-actions-dropdown 54 <my-video-actions-dropdown
55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left" 55 *ngIf="showActions" [video]="video" [displayOptions]="videoActionsDisplayOptions" placement="bottom-left bottom-right left"
56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()" 56 (videoRemoved)="onVideoRemoved()" (videoBlacklisted)="onVideoBlacklisted()" (videoUnblacklisted)="onVideoUnblacklisted()"
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/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
new file mode 100644
index 000000000..d78e1ab90
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -0,0 +1,626 @@
1// Thanks https://github.com/streamroot/videojs-hlsjs-plugin
2// We duplicated this plugin to choose the hls.js version we want, because streamroot only provide a bundled file
3
4import * as Hlsjs from 'hls.js'
5import videojs, { VideoJsPlayer } from 'video.js'
6import { HlsjsConfigHandlerOptions, QualityLevelRepresentation, QualityLevels, VideoJSTechHLS } from '../peertube-videojs-typings'
7
8type ErrorCounts = {
9 [ type: string ]: number
10}
11
12type Metadata = {
13 levels: Hlsjs.Level[]
14}
15
16const registerSourceHandler = function (vjs: typeof videojs) {
17 if (!Hlsjs.isSupported()) {
18 console.warn('Hls.js is not supported in this browser!')
19 return
20 }
21
22 const html5 = vjs.getTech('Html5')
23
24 if (!html5) {
25 console.error('Not supported version if video.js')
26 return
27 }
28
29 // FIXME: typings
30 (html5 as any).registerSourceHandler({
31 canHandleSource: function (source: videojs.Tech.SourceObject) {
32 const hlsTypeRE = /^application\/x-mpegURL|application\/vnd\.apple\.mpegurl$/i
33 const hlsExtRE = /\.m3u8/i
34
35 if (hlsTypeRE.test(source.type)) return 'probably'
36 if (hlsExtRE.test(source.src)) return 'maybe'
37
38 return ''
39 },
40
41 handleSource: function (source: videojs.Tech.SourceObject, tech: VideoJSTechHLS) {
42 if (tech.hlsProvider) {
43 tech.hlsProvider.dispose()
44 }
45
46 tech.hlsProvider = new Html5Hlsjs(vjs, source, tech)
47
48 return tech.hlsProvider
49 }
50 }, 0);
51
52 // FIXME: typings
53 (vjs as any).Html5Hlsjs = Html5Hlsjs
54}
55
56function hlsjsConfigHandler (this: VideoJsPlayer, options: HlsjsConfigHandlerOptions) {
57 const player = this
58
59 if (!options) return
60
61 if (!player.srOptions_) {
62 player.srOptions_ = {}
63 }
64
65 if (!player.srOptions_.hlsjsConfig) {
66 player.srOptions_.hlsjsConfig = options.hlsjsConfig
67 }
68
69 if (!player.srOptions_.captionConfig) {
70 player.srOptions_.captionConfig = options.captionConfig
71 }
72
73 if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) {
74 player.srOptions_.levelLabelHandler = options.levelLabelHandler
75 }
76}
77
78const registerConfigPlugin = function (vjs: typeof videojs) {
79 // Used in Brightcove since we don't pass options directly there
80 const registerVjsPlugin = vjs.registerPlugin || vjs.plugin
81 registerVjsPlugin('hlsjs', hlsjsConfigHandler)
82}
83
84class Html5Hlsjs {
85 private static readonly hooks: { [id: string]: Function[] } = {}
86
87 private readonly videoElement: HTMLVideoElement
88 private readonly errorCounts: ErrorCounts = {}
89 private readonly player: VideoJsPlayer
90 private readonly tech: videojs.Tech
91 private readonly source: videojs.Tech.SourceObject
92 private readonly vjs: typeof videojs
93
94 private hls: Hlsjs & { manualLevel?: number } // FIXME: typings
95 private hlsjsConfig: Partial<Hlsjs.Config & { cueHandler: any }> = null
96
97 private _duration: number = null
98 private metadata: Metadata = null
99 private isLive: boolean = null
100 private dvrDuration: number = null
101 private edgeMargin: number = null
102
103 private handlers: { [ id in 'play' | 'addtrack' | 'playing' | 'textTracksChange' | 'audioTracksChange' ]: EventListener } = {
104 play: null,
105 addtrack: null,
106 playing: null,
107 textTracksChange: null,
108 audioTracksChange: null
109 }
110
111 private uiTextTrackHandled = false
112
113 constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) {
114 this.vjs = vjs
115 this.source = source
116
117 this.tech = tech;
118 (this.tech as any).name_ = 'Hlsjs'
119
120 this.videoElement = tech.el() as HTMLVideoElement
121 this.player = vjs((tech.options_ as any).playerId)
122
123 this.videoElement.addEventListener('error', event => {
124 let errorTxt: string
125 const mediaError = (event.currentTarget as HTMLVideoElement).error
126
127 switch (mediaError.code) {
128 case mediaError.MEDIA_ERR_ABORTED:
129 errorTxt = 'You aborted the video playback'
130 break
131 case mediaError.MEDIA_ERR_DECODE:
132 errorTxt = 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support'
133 this._handleMediaError(mediaError)
134 break
135 case mediaError.MEDIA_ERR_NETWORK:
136 errorTxt = 'A network error caused the video download to fail part-way'
137 break
138 case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
139 errorTxt = 'The video could not be loaded, either because the server or network failed or because the format is not supported'
140 break
141
142 default:
143 errorTxt = mediaError.message
144 }
145
146 console.error('MEDIA_ERROR: ', errorTxt)
147 })
148
149 this.initialize()
150 }
151
152 duration () {
153 return this._duration || this.videoElement.duration || 0
154 }
155
156 seekable () {
157 if (this.hls.media) {
158 if (!this.isLive) {
159 return this.vjs.createTimeRanges(0, this.hls.media.duration)
160 }
161
162 // Video.js doesn't seem to like floating point timeranges
163 const startTime = Math.round(this.hls.media.duration - this.dvrDuration)
164 const endTime = Math.round(this.hls.media.duration - this.edgeMargin)
165
166 return this.vjs.createTimeRanges(startTime, endTime)
167 }
168
169 return this.vjs.createTimeRanges()
170 }
171
172 // See comment for `initialize` method.
173 dispose () {
174 this.videoElement.removeEventListener('play', this.handlers.play)
175 this.videoElement.textTracks.removeEventListener('addtrack', this.handlers.addtrack)
176 this.videoElement.removeEventListener('playing', this.handlers.playing)
177
178 this.player.textTracks().removeEventListener('change', this.handlers.textTracksChange)
179 this.uiTextTrackHandled = false
180
181 this.player.audioTracks().removeEventListener('change', this.handlers.audioTracksChange)
182
183 this.hls.destroy()
184 }
185
186 static addHook (type: string, callback: Function) {
187 Html5Hlsjs.hooks[ type ] = this.hooks[ type ] || []
188 Html5Hlsjs.hooks[ type ].push(callback)
189 }
190
191 static removeHook (type: string, callback: Function) {
192 if (Html5Hlsjs.hooks[ type ] === undefined) return false
193
194 const index = Html5Hlsjs.hooks[ type ].indexOf(callback)
195 if (index === -1) return false
196
197 Html5Hlsjs.hooks[ type ].splice(index, 1)
198
199 return true
200 }
201
202 private _executeHooksFor (type: string) {
203 if (Html5Hlsjs.hooks[ type ] === undefined) {
204 return
205 }
206
207 // ES3 and IE < 9
208 for (let i = 0; i < Html5Hlsjs.hooks[ type ].length; i++) {
209 Html5Hlsjs.hooks[ type ][ i ](this.player, this.hls)
210 }
211 }
212
213 private _handleMediaError (error: any) {
214 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 1) {
215 console.info('trying to recover media error')
216 this.hls.recoverMediaError()
217 return
218 }
219
220 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 2) {
221 console.info('2nd try to recover media error (by swapping audio codec')
222 this.hls.swapAudioCodec()
223 this.hls.recoverMediaError()
224 return
225 }
226
227 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] > 2) {
228 console.info('bubbling media error up to VIDEOJS')
229 this.tech.error = () => error
230 this.tech.trigger('error')
231 return
232 }
233 }
234
235 private _onError (_event: any, data: Hlsjs.errorData) {
236 const error: { message: string, code?: number } = {
237 message: `HLS.js error: ${data.type} - fatal: ${data.fatal} - ${data.details}`
238 }
239 console.error(error.message)
240
241 // increment/set error count
242 if (this.errorCounts[ data.type ]) this.errorCounts[ data.type ] += 1
243 else this.errorCounts[ data.type ] = 1
244
245 // Implement simple error handling based on hls.js documentation
246 // https://github.com/dailymotion/hls.js/blob/master/API.md#fifth-step-error-handling
247 if (data.fatal) {
248 switch (data.type) {
249 case Hlsjs.ErrorTypes.NETWORK_ERROR:
250 console.info('bubbling network error up to VIDEOJS')
251 error.code = 2
252 this.tech.error = () => error as any
253 this.tech.trigger('error')
254 break
255
256 case Hlsjs.ErrorTypes.MEDIA_ERROR:
257 error.code = 3
258 this._handleMediaError(error)
259 break
260
261 default:
262 // cannot recover
263 this.hls.destroy()
264 console.info('bubbling error up to VIDEOJS')
265 this.tech.error = () => error as any
266 this.tech.trigger('error')
267 break
268 }
269 }
270 }
271
272 private switchQuality (qualityId: number) {
273 this.hls.nextLevel = qualityId
274 }
275
276 private _levelLabel (level: Hlsjs.Level) {
277 if (this.player.srOptions_.levelLabelHandler) {
278 return this.player.srOptions_.levelLabelHandler(level)
279 }
280
281 if (level.height) return level.height + 'p'
282 if (level.width) return Math.round(level.width * 9 / 16) + 'p'
283 if (level.bitrate) return (level.bitrate / 1000) + 'kbps'
284
285 return 0
286 }
287
288 private _relayQualityChange (qualityLevels: QualityLevels) {
289 // Determine if it is "Auto" (all tracks enabled)
290 let isAuto = true
291
292 for (let i = 0; i < qualityLevels.length; i++) {
293 if (!qualityLevels[ i ]._enabled) {
294 isAuto = false
295 break
296 }
297 }
298
299 // Interact with ME
300 if (isAuto) {
301 this.hls.currentLevel = -1
302 return
303 }
304
305 // Find ID of highest enabled track
306 let selectedTrack: number
307
308 for (selectedTrack = qualityLevels.length - 1; selectedTrack >= 0; selectedTrack--) {
309 if (qualityLevels[ selectedTrack ]._enabled) {
310 break
311 }
312 }
313
314 this.hls.currentLevel = selectedTrack
315 }
316
317 private _handleQualityLevels () {
318 if (!this.metadata) return
319
320 const qualityLevels = this.player.qualityLevels && this.player.qualityLevels()
321 if (!qualityLevels) return
322
323 for (let i = 0; i < this.metadata.levels.length; i++) {
324 const details = this.metadata.levels[ i ]
325 const representation: QualityLevelRepresentation = {
326 id: i,
327 width: details.width,
328 height: details.height,
329 bandwidth: details.bitrate,
330 bitrate: details.bitrate,
331 _enabled: true
332 }
333
334 const self = this
335 representation.enabled = function (this: QualityLevels, level: number, toggle?: boolean) {
336 // Brightcove switcher works TextTracks-style (enable tracks that it wants to ABR on)
337 if (typeof toggle === 'boolean') {
338 this[ level ]._enabled = toggle
339 self._relayQualityChange(this)
340 }
341
342 return this[ level ]._enabled
343 }
344
345 qualityLevels.addQualityLevel(representation)
346 }
347 }
348
349 private _notifyVideoQualities () {
350 if (!this.metadata) return
351 const cleanTracklist = []
352
353 if (this.metadata.levels.length > 1) {
354 const autoLevel = {
355 id: -1,
356 label: 'auto',
357 selected: this.hls.manualLevel === -1
358 }
359 cleanTracklist.push(autoLevel)
360 }
361
362 this.metadata.levels.forEach((level, index) => {
363 // Don't write in level (shared reference with Hls.js)
364 const quality = {
365 id: index,
366 selected: index === this.hls.manualLevel,
367 label: this._levelLabel(level)
368 }
369
370 cleanTracklist.push(quality)
371 })
372
373 const payload = {
374 qualityData: { video: cleanTracklist },
375 qualitySwitchCallback: this.switchQuality.bind(this)
376 }
377
378 this.tech.trigger('loadedqualitydata', payload)
379
380 // Self-de-register so we don't raise the payload multiple times
381 this.videoElement.removeEventListener('playing', this.handlers.playing)
382 }
383
384 private _updateSelectedAudioTrack () {
385 const playerAudioTracks = this.tech.audioTracks()
386 for (let j = 0; j < playerAudioTracks.length; j++) {
387 // FIXME: typings
388 if ((playerAudioTracks[ j ] as any).enabled) {
389 this.hls.audioTrack = j
390 break
391 }
392 }
393 }
394
395 private _onAudioTracks () {
396 const hlsAudioTracks = this.hls.audioTracks as (AudioTrack & { name?: string, lang?: string })[] // FIXME typings
397 const playerAudioTracks = this.tech.audioTracks()
398
399 if (hlsAudioTracks.length > 1 && playerAudioTracks.length === 0) {
400 // Add Hls.js audio tracks if not added yet
401 for (let i = 0; i < hlsAudioTracks.length; i++) {
402 playerAudioTracks.addTrack(new this.vjs.AudioTrack({
403 id: i.toString(),
404 kind: 'alternative',
405 label: hlsAudioTracks[ i ].name || hlsAudioTracks[ i ].lang,
406 language: hlsAudioTracks[ i ].lang,
407 enabled: i === this.hls.audioTrack
408 }))
409 }
410
411 // Handle audio track change event
412 this.handlers.audioTracksChange = this._updateSelectedAudioTrack.bind(this)
413 playerAudioTracks.addEventListener('change', this.handlers.audioTracksChange)
414 }
415 }
416
417 private _getTextTrackLabel (textTrack: TextTrack) {
418 // Label here is readable label and is optional (used in the UI so if it is there it should be different)
419 return textTrack.label ? textTrack.label : textTrack.language
420 }
421
422 private _isSameTextTrack (track1: TextTrack, track2: TextTrack) {
423 return this._getTextTrackLabel(track1) === this._getTextTrackLabel(track2)
424 && track1.kind === track2.kind
425 }
426
427 private _updateSelectedTextTrack () {
428 const playerTextTracks = this.player.textTracks()
429 let activeTrack: TextTrack = null
430
431 for (let j = 0; j < playerTextTracks.length; j++) {
432 if (playerTextTracks[ j ].mode === 'showing') {
433 activeTrack = playerTextTracks[ j ]
434 break
435 }
436 }
437
438 const hlsjsTracks = this.videoElement.textTracks
439 for (let k = 0; k < hlsjsTracks.length; k++) {
440 if (hlsjsTracks[ k ].kind === 'subtitles' || hlsjsTracks[ k ].kind === 'captions') {
441 hlsjsTracks[ k ].mode = activeTrack && this._isSameTextTrack(hlsjsTracks[ k ], activeTrack)
442 ? 'showing'
443 : 'disabled'
444 }
445 }
446 }
447
448 private _startLoad () {
449 this.hls.startLoad(-1)
450 this.videoElement.removeEventListener('play', this.handlers.play)
451 }
452
453 private _oneLevelObjClone (obj: object) {
454 const result = {}
455 const objKeys = Object.keys(obj)
456 for (let i = 0; i < objKeys.length; i++) {
457 result[ objKeys[ i ] ] = obj[ objKeys[ i ] ]
458 }
459
460 return result
461 }
462
463 private _filterDisplayableTextTracks (textTracks: TextTrackList) {
464 const displayableTracks = []
465
466 // Filter out tracks that is displayable (captions or subtitles)
467 for (let idx = 0; idx < textTracks.length; idx++) {
468 if (textTracks[ idx ].kind === 'subtitles' || textTracks[ idx ].kind === 'captions') {
469 displayableTracks.push(textTracks[ idx ])
470 }
471 }
472
473 return displayableTracks
474 }
475
476 private _updateTextTrackList () {
477 const displayableTracks = this._filterDisplayableTextTracks(this.videoElement.textTracks)
478 const playerTextTracks = this.player.textTracks()
479
480 // Add stubs to make the caption switcher shows up
481 // Adding the Hls.js text track in will make us have double captions
482 for (let idx = 0; idx < displayableTracks.length; idx++) {
483 let isAdded = false
484
485 for (let jdx = 0; jdx < playerTextTracks.length; jdx++) {
486 if (this._isSameTextTrack(displayableTracks[ idx ], playerTextTracks[ jdx ])) {
487 isAdded = true
488 break
489 }
490 }
491
492 if (!isAdded) {
493 const hlsjsTextTrack = displayableTracks[ idx ]
494 this.player.addRemoteTextTrack({
495 kind: hlsjsTextTrack.kind as videojs.TextTrack.Kind,
496 label: this._getTextTrackLabel(hlsjsTextTrack),
497 language: hlsjsTextTrack.language,
498 srclang: hlsjsTextTrack.language
499 }, false)
500 }
501 }
502
503 // Handle UI switching
504 this._updateSelectedTextTrack()
505
506 if (!this.uiTextTrackHandled) {
507 this.handlers.textTracksChange = this._updateSelectedTextTrack.bind(this)
508 playerTextTracks.addEventListener('change', this.handlers.textTracksChange)
509
510 this.uiTextTrackHandled = true
511 }
512 }
513
514 private _onMetaData (_event: any, data: Hlsjs.manifestLoadedData) {
515 // This could arrive before 'loadedqualitydata' handlers is registered, remember it so we can raise it later
516 this.metadata = data as any
517 this._handleQualityLevels()
518 }
519
520 private _createCueHandler (captionConfig: any) {
521 return {
522 newCue: (track: any, startTime: number, endTime: number, captionScreen: { rows: any[] }) => {
523 let row: any
524 let cue: VTTCue
525 let text: string
526 const VTTCue = (window as any).VTTCue || (window as any).TextTrackCue
527
528 for (let r = 0; r < captionScreen.rows.length; r++) {
529 row = captionScreen.rows[ r ]
530 text = ''
531
532 if (!row.isEmpty()) {
533 for (let c = 0; c < row.chars.length; c++) {
534 text += row.chars[ c ].ucharj
535 }
536
537 cue = new VTTCue(startTime, endTime, text.trim())
538
539 // typeof null === 'object'
540 if (captionConfig != null && typeof captionConfig === 'object') {
541 // Copy client overridden property into the cue object
542 const configKeys = Object.keys(captionConfig)
543
544 for (let k = 0; k < configKeys.length; k++) {
545 cue[ configKeys[ k ] ] = captionConfig[ configKeys[ k ] ]
546 }
547 }
548 track.addCue(cue)
549 if (endTime === startTime) track.addCue(new VTTCue(endTime + 5, ''))
550 }
551 }
552 }
553 }
554 }
555
556 private _initHlsjs () {
557 const techOptions = this.tech.options_ as HlsjsConfigHandlerOptions
558 const srOptions_ = this.player.srOptions_
559
560 const hlsjsConfigRef = srOptions_ && srOptions_.hlsjsConfig || techOptions.hlsjsConfig
561 // Hls.js will write to the reference thus change the object for later streams
562 this.hlsjsConfig = hlsjsConfigRef ? this._oneLevelObjClone(hlsjsConfigRef) : {}
563
564 if ([ '', 'auto' ].includes(this.videoElement.preload) && !this.videoElement.autoplay && this.hlsjsConfig.autoStartLoad === undefined) {
565 this.hlsjsConfig.autoStartLoad = false
566 }
567
568 const captionConfig = srOptions_ && srOptions_.captionConfig || techOptions.captionConfig
569 if (captionConfig) {
570 this.hlsjsConfig.cueHandler = this._createCueHandler(captionConfig)
571 }
572
573 // If the user explicitly sets autoStartLoad to false, we're not going to enter the if block above
574 // That's why we have a separate if block here to set the 'play' listener
575 if (this.hlsjsConfig.autoStartLoad === false) {
576 this.handlers.play = this._startLoad.bind(this)
577 this.videoElement.addEventListener('play', this.handlers.play)
578 }
579
580 // _notifyVideoQualities sometimes runs before the quality picker event handler is registered -> no video switcher
581 this.handlers.playing = this._notifyVideoQualities.bind(this)
582 this.videoElement.addEventListener('playing', this.handlers.playing)
583
584 this.hls = new Hlsjs(this.hlsjsConfig)
585
586 this._executeHooksFor('beforeinitialize')
587
588 this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
589 this.hls.on(Hlsjs.Events.AUDIO_TRACKS_UPDATED, () => this._onAudioTracks())
590 this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data as any)) // FIXME: typings
591 this.hls.on(Hlsjs.Events.LEVEL_LOADED, (event, data) => {
592 // The DVR plugin will auto seek to "live edge" on start up
593 if (this.hlsjsConfig.liveSyncDuration) {
594 this.edgeMargin = this.hlsjsConfig.liveSyncDuration
595 } else if (this.hlsjsConfig.liveSyncDurationCount) {
596 this.edgeMargin = this.hlsjsConfig.liveSyncDurationCount * data.details.targetduration
597 }
598
599 this.isLive = data.details.live
600 this.dvrDuration = data.details.totalduration
601 this._duration = this.isLive ? Infinity : data.details.totalduration
602 })
603 this.hls.once(Hlsjs.Events.FRAG_LOADED, () => {
604 // Emit custom 'loadedmetadata' event for parity with `videojs-contrib-hls`
605 // Ref: https://github.com/videojs/videojs-contrib-hls#loadedmetadata
606 this.tech.trigger('loadedmetadata')
607 })
608
609 this.hls.attachMedia(this.videoElement)
610
611 this.handlers.addtrack = this._updateTextTrackList.bind(this)
612 this.videoElement.textTracks.addEventListener('addtrack', this.handlers.addtrack)
613
614 this.hls.loadSource(this.source.src)
615 }
616
617 private initialize () {
618 this._initHlsjs()
619 }
620}
621
622export {
623 Html5Hlsjs,
624 registerSourceHandler,
625 registerConfigPlugin
626}
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..e86900faa 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,16 +1,15 @@
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'
6import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
7import * as Hlsjs from 'hls.js'
8 8
9// videojs-hlsjs-plugin needs videojs in window 9registerConfigPlugin(videojs)
10window['videojs'] = videojs 10registerSourceHandler(videojs)
11require('@streamroot/videojs-hlsjs-plugin')
12 11
13const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 12const Plugin = videojs.getPlugin('plugin')
14class P2pMediaLoaderPlugin extends Plugin { 13class P2pMediaLoaderPlugin extends Plugin {
15 14
16 private readonly CONSTANTS = { 15 private readonly CONSTANTS = {
@@ -18,7 +17,7 @@ class P2pMediaLoaderPlugin extends Plugin {
18 } 17 }
19 private readonly options: P2PMediaLoaderPluginOptions 18 private readonly options: P2PMediaLoaderPluginOptions
20 19
21 private hlsjs: any // Don't type hlsjs to not bundle the module 20 private hlsjs: Hlsjs
22 private p2pEngine: Engine 21 private p2pEngine: Engine
23 private statsP2PBytes = { 22 private statsP2PBytes = {
24 pendingDownload: [] as number[], 23 pendingDownload: [] as number[],
@@ -37,12 +36,13 @@ class P2pMediaLoaderPlugin extends Plugin {
37 36
38 private networkInfoInterval: any 37 private networkInfoInterval: any
39 38
40 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { 39 constructor (player: VideoJsPlayer, options?: P2PMediaLoaderPluginOptions) {
41 super(player, options) 40 super(player)
42 41
43 this.options = options 42 this.options = options
44 43
45 if (!videojs.Html5Hlsjs) { 44 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
45 if (!(videojs as any).Html5Hlsjs) {
46 const message = 'HLS.js does not seem to be supported.' 46 const message = 'HLS.js does not seem to be supported.'
47 console.warn(message) 47 console.warn(message)
48 48
@@ -50,7 +50,8 @@ class P2pMediaLoaderPlugin extends Plugin {
50 return 50 return
51 } 51 }
52 52
53 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { 53 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
54 (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
54 this.hlsjs = hlsjs 55 this.hlsjs = hlsjs
55 }) 56 })
56 57
@@ -84,12 +85,11 @@ class P2pMediaLoaderPlugin extends Plugin {
84 private initialize () { 85 private initialize () {
85 initHlsJsPlayer(this.hlsjs) 86 initHlsJsPlayer(this.hlsjs)
86 87
87 const tech = this.player.tech_ 88 // FIXME: typings
88 this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() 89 const options = this.player.tech(true).options_ as any
90 this.p2pEngine = options.hlsjsConfig.loader.getEngine()
89 91
90 // Avoid using constants to not import hls.hs 92 this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHING, (_: any, data: any) => {
91 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37
92 this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => {
93 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) 93 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
94 }) 94 })
95 95
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index d9e02cd7d..dbf631a5e 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
@@ -197,9 +211,9 @@ export class PeertubePlayerManager {
197 } 211 }
198 212
199 if (mode === 'p2p-media-loader') { 213 if (mode === 'p2p-media-loader') {
200 const { streamrootHls } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) 214 const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
201 215
202 html5 = streamrootHls.html5 216 html5 = hlsjs.html5
203 } 217 }
204 218
205 if (mode === 'webtorrent') { 219 if (mode === 'webtorrent') {
@@ -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
@@ -289,7 +303,7 @@ export class PeertubePlayerManager {
289 swarmId: p2pMediaLoaderOptions.playlistUrl 303 swarmId: p2pMediaLoaderOptions.playlistUrl
290 } 304 }
291 } 305 }
292 const streamrootHls = { 306 const hlsjs = {
293 levelLabelHandler: (level: { height: number, width: number }) => { 307 levelLabelHandler: (level: { height: number, width: number }) => {
294 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) 308 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height)
295 309
@@ -308,7 +322,7 @@ export class PeertubePlayerManager {
308 } 322 }
309 } 323 }
310 324
311 const toAssign = { p2pMediaLoader, streamrootHls } 325 const toAssign = { p2pMediaLoader, hlsjs }
312 Object.assign(plugins, toAssign) 326 Object.assign(plugins, toAssign)
313 327
314 return toAssign 328 return toAssign
@@ -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..a4e4c580c 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,28 +1,81 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { PeerTubePlugin } from './peertube-plugin' 1import { PeerTubePlugin } from './peertube-plugin'
6import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' 2import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
7import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 3import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
8import { PlayerMode } from './peertube-player-manager' 4import { PlayerMode } from './peertube-player-manager'
9import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 5import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
10import { VideoFile } from '@shared/models' 6import { VideoFile } from '@shared/models'
7import videojs from 'video.js'
8import { Config, Level } from 'hls.js'
9
10declare module 'video.js' {
11
12 export interface VideoJsPlayer {
13 srOptions_: HlsjsConfigHandlerOptions
14
15 theaterEnabled: boolean
16
17 // FIXME: add it to upstream typings
18 posterImage: {
19 show (): void
20 hide (): void
21 }
22
23 handleTechSeeked_ (): void
24
25 // Plugins
11 26
12declare namespace videojs {
13 interface Player {
14 peertube (): PeerTubePlugin 27 peertube (): PeerTubePlugin
28
15 webtorrent (): WebTorrentPlugin 29 webtorrent (): WebTorrentPlugin
30
16 p2pMediaLoader (): P2pMediaLoaderPlugin 31 p2pMediaLoader (): P2pMediaLoaderPlugin
32
33 contextmenuUI (options: any): any
34
35 bezels (): void
36
37 qualityLevels (): QualityLevels
38
39 textTracks (): TextTrackList & {
40 on: Function
41 tracks_: { kind: string, mode: string, language: string }[]
42 }
43
44 audioTracks (): AudioTrackList
45
46 dock (options: { title: string, description: string }): void
17 } 47 }
18} 48}
19 49
20interface VideoJSComponentInterface { 50export interface VideoJSTechHLS extends videojs.Tech {
21 _player: videojs.Player 51 hlsProvider: any // FIXME: typings
52}
53
54export interface HlsjsConfigHandlerOptions {
55 hlsjsConfig?: Config & { cueHandler: any }// FIXME: typings
56 captionConfig?: any // FIXME: typings
57
58 levelLabelHandler?: (level: Level) => string
59}
60
61type QualityLevelRepresentation = {
62 id: number
63 height: number
64
65 label?: string
66 width?: number
67 bandwidth?: number
68 bitrate?: number
22 69
23 new (player: videojs.Player, options?: any): any 70 enabled?: Function
71 _enabled: boolean
72}
73
74type QualityLevels = QualityLevelRepresentation[] & {
75 selectedIndex: number
76 selectedIndex_: number
24 77
25 registerComponent (name: string, obj: any): any 78 addQualityLevel (representation: QualityLevelRepresentation): void
26} 79}
27 80
28type VideoJSCaption = { 81type VideoJSCaption = {
@@ -78,9 +131,6 @@ type VideoJSPluginOptions = {
78 p2pMediaLoader?: P2PMediaLoaderPluginOptions 131 p2pMediaLoader?: P2PMediaLoaderPluginOptions
79} 132}
80 133
81// videojs typings don't have some method we need
82const videojsUntyped = videojs as any
83
84type LoadedQualityData = { 134type LoadedQualityData = {
85 qualitySwitchCallback: Function, 135 qualitySwitchCallback: Function,
86 qualityData: { 136 qualityData: {
@@ -123,13 +173,13 @@ export {
123 PlayerNetworkInfo, 173 PlayerNetworkInfo,
124 ResolutionUpdateData, 174 ResolutionUpdateData,
125 AutoResolutionUpdateData, 175 AutoResolutionUpdateData,
126 VideoJSComponentInterface,
127 videojsUntyped,
128 VideoJSCaption, 176 VideoJSCaption,
129 UserWatching, 177 UserWatching,
130 PeerTubePluginOptions, 178 PeerTubePluginOptions,
131 WebtorrentPluginOptions, 179 WebtorrentPluginOptions,
132 P2PMediaLoaderPluginOptions, 180 P2PMediaLoaderPluginOptions,
133 VideoJSPluginOptions, 181 VideoJSPluginOptions,
134 LoadedQualityData 182 LoadedQualityData,
183 QualityLevelRepresentation,
184 QualityLevels
135} 185}
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..bf6b0a718 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,14 +22,16 @@ 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 {
28 readonly videoFiles: VideoFile[]
29
29 private readonly playerElement: HTMLVideoElement 30 private readonly playerElement: HTMLVideoElement
30 31
31 private readonly autoplay: boolean = false 32 private readonly autoplay: boolean = false
32 private readonly startTime: number = 0 33 private readonly startTime: number = 0
33 private readonly savePlayerSrcFunction: Function 34 private readonly savePlayerSrcFunction: VideoJsPlayer['src']
34 private readonly videoFiles: VideoFile[]
35 private readonly videoDuration: number 35 private readonly videoDuration: number
36 private readonly CONSTANTS = { 36 private readonly CONSTANTS = {
37 INFO_SCHEDULER: 1000, // Don't change this 37 INFO_SCHEDULER: 1000, // Don't change this
@@ -49,7 +49,6 @@ class WebTorrentPlugin extends Plugin {
49 dht: false 49 dht: false
50 }) 50 })
51 51
52 private player: any
53 private currentVideoFile: VideoFile 52 private currentVideoFile: VideoFile
54 private torrent: WebTorrent.Torrent 53 private torrent: WebTorrent.Torrent
55 54
@@ -70,8 +69,8 @@ class WebTorrentPlugin extends Plugin {
70 69
71 private downloadSpeeds: number[] = [] 70 private downloadSpeeds: number[] = []
72 71
73 constructor (player: videojs.Player, options: WebtorrentPluginOptions) { 72 constructor (player: VideoJsPlayer, options?: WebtorrentPluginOptions) {
74 super(player, options) 73 super(player)
75 74
76 this.startTime = timeToInt(options.startTime) 75 this.startTime = timeToInt(options.startTime)
77 76
@@ -147,12 +146,12 @@ class WebTorrentPlugin extends Plugin {
147 } 146 }
148 147
149 // Do not display error to user because we will have multiple fallback 148 // Do not display error to user because we will have multiple fallback
150 this.disableErrorDisplay() 149 this.disableErrorDisplay();
151 150
152 // Hack to "simulate" src link in video.js >= 6 151 // Hack to "simulate" src link in video.js >= 6
153 // Without this, we can't play the video after pausing it 152 // 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 153 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
155 this.player.src = () => true 154 (this.player as any).src = () => true
156 const oldPlaybackRate = this.player.playbackRate() 155 const oldPlaybackRate = this.player.playbackRate()
157 156
158 const previousVideoFile = this.currentVideoFile 157 const previousVideoFile = this.currentVideoFile
@@ -333,7 +332,7 @@ class WebTorrentPlugin extends Plugin {
333 332
334 const playPromise = this.player.play() 333 const playPromise = this.player.play()
335 if (playPromise !== undefined) { 334 if (playPromise !== undefined) {
336 return playPromise.then(done) 335 return playPromise.then(() => done())
337 .catch((err: Error) => { 336 .catch((err: Error) => {
338 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { 337 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) {
339 return 338 return
@@ -426,8 +425,8 @@ class WebTorrentPlugin extends Plugin {
426 } 425 }
427 426
428 // Proxy first play 427 // Proxy first play
429 const oldPlay = this.player.play.bind(this.player) 428 const oldPlay = this.player.play.bind(this.player);
430 this.player.play = () => { 429 (this.player as any).play = () => {
431 this.player.addClass('vjs-has-big-play-button-clicked') 430 this.player.addClass('vjs-has-big-play-button-clicked')
432 this.player.play = oldPlay 431 this.player.play = oldPlay
433 432
@@ -619,7 +618,7 @@ class WebTorrentPlugin extends Plugin {
619 video: qualityLevelsPayload 618 video: qualityLevelsPayload
620 } 619 }
621 } 620 }
622 this.player.tech_.trigger('loadedqualitydata', payload) 621 this.player.tech(true).trigger('loadedqualitydata', payload)
623 } 622 }
624 623
625 private buildQualityLabel (file: VideoFile) { 624 private buildQualityLabel (file: VideoFile) {
@@ -651,9 +650,9 @@ class WebTorrentPlugin extends Plugin {
651 return 650 return
652 } 651 }
653 652
654 for (let i = 0; i < qualityLevels; i++) { 653 for (let i = 0; i < qualityLevels.length; i++) {
655 const q = this.player.qualityLevels[i] 654 const q = qualityLevels[i]
656 if (q.height === resolutionId) qualityLevels.selectedIndex = i 655 if (q.height === resolutionId) qualityLevels.selectedIndex_ = i
657 } 656 }
658 } 657 }
659} 658}
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index 5dacdd73b..fa2452231 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -32,7 +32,7 @@ body {
32 --menuForegroundColor: #{$menu-color}; 32 --menuForegroundColor: #{$menu-color};
33 --submenuColor: #{$sub-menu-color}; 33 --submenuColor: #{$sub-menu-color};
34 34
35 --inputColor: #{$input-background-color}; 35 --inputBackgroundColor: #{$input-background-color};
36 --inputPlaceholderColor: #{$input-placeholder-color}; 36 --inputPlaceholderColor: #{$input-placeholder-color};
37 37
38 --actionButtonColor: #{$grey-foreground-color}; 38 --actionButtonColor: #{$grey-foreground-color};
@@ -61,7 +61,7 @@ strong {
61 61
62input.readonly { 62input.readonly {
63 /* Force blank on readonly inputs */ 63 /* Force blank on readonly inputs */
64 background-color: var(--inputColor) !important; 64 background-color: var(--inputBackgroundColor) !important;
65} 65}
66 66
67input, textarea { 67input, textarea {
@@ -202,26 +202,6 @@ label {
202 to { transform: scale(1) rotate(360deg);} 202 to { transform: scale(1) rotate(360deg);}
203} 203}
204 204
205.orange-button {
206 @include peertube-button;
207 @include orange-button;
208}
209
210.orange-button-link {
211 @include peertube-button-link;
212 @include orange-button;
213}
214
215.grey-button {
216 @include peertube-button;
217 @include grey-button;
218}
219
220.grey-button-link {
221 @include peertube-button-link;
222 @include grey-button;
223}
224
225// In tables, don't have a hover different background 205// In tables, don't have a hover different background
226table { 206table {
227 .action-button-edit, .action-button-delete { 207 .action-button-edit, .action-button-delete {
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 136eddd3a..317781e0e 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -77,11 +77,17 @@
77 } 77 }
78} 78}
79 79
80@mixin button-focus-visible-shadow($color) {
81 &.focus-visible {
82 box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px $color;
83 }
84}
85
80@mixin peertube-input-text($width) { 86@mixin peertube-input-text($width) {
81 display: inline-block; 87 display: inline-block;
82 height: $button-height; 88 height: $button-height;
83 width: $width; 89 width: $width;
84 background: var(--inputColor); 90 background: var(--inputBackgroundColor);
85 border: 1px solid #C6C6C6; 91 border: 1px solid #C6C6C6;
86 border-radius: 3px; 92 border-radius: 3px;
87 padding-left: 15px; 93 padding-left: 15px;
@@ -118,6 +124,8 @@
118} 124}
119 125
120@mixin orange-button { 126@mixin orange-button {
127 @include button-focus-visible-shadow(var(--mainHoverColor));
128
121 &, &:active, &:focus { 129 &, &:active, &:focus {
122 color: #fff; 130 color: #fff;
123 background-color: var(--mainColor); 131 background-color: var(--mainColor);
@@ -169,7 +177,6 @@
169 text-align: center; 177 text-align: center;
170 padding: 0 17px 0 13px; 178 padding: 0 17px 0 13px;
171 cursor: pointer; 179 cursor: pointer;
172 outline: 0;
173} 180}
174 181
175@mixin peertube-button-link { 182@mixin peertube-button-link {
@@ -254,7 +261,7 @@
254 width: $width; 261 width: $width;
255 border-radius: 3px; 262 border-radius: 3px;
256 overflow: hidden; 263 overflow: hidden;
257 background: var(--inputColor); 264 background: var(--inputBackgroundColor);
258 position: relative; 265 position: relative;
259 font-size: 15px; 266 font-size: 15px;
260 267
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index 5b5ac9adc..e087a2548 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -81,7 +81,7 @@ $variables: (
81 --menuForegroundColor: var(--menuForegroundColor), 81 --menuForegroundColor: var(--menuForegroundColor),
82 --submenuColor: var(--submenuColor), 82 --submenuColor: var(--submenuColor),
83 83
84 --inputColor: var(--inputColor), 84 --inputBackgroundColor: var(--inputBackgroundColor),
85 --inputPlaceholderColor: var(--inputPlaceholderColor), 85 --inputPlaceholderColor: var(--inputPlaceholderColor),
86 86
87 --actionButtonColor: var(--actionButtonColor), 87 --actionButtonColor: var(--actionButtonColor),
diff --git a/client/src/sass/player/_player-variables.scss b/client/src/sass/player/_player-variables.scss
index 0c2359ac7..935a60b49 100644
--- a/client/src/sass/player/_player-variables.scss
+++ b/client/src/sass/player/_player-variables.scss
@@ -1,7 +1,7 @@
1$primary-foreground-color: #fff; 1$primary-foreground-color: #fff;
2$primary-foreground-opacity: 0.9; 2$primary-foreground-opacity: 0.9;
3$primary-foreground-opacity-hover: 1; 3$primary-foreground-opacity-hover: 1;
4$primary-background-color: #000; 4$primary-background-color: rgba(0, 0, 0, 0.8);
5 5
6$font-size: 13px; 6$font-size: 13px;
7$control-bar-height: 34px; 7$control-bar-height: 34px;
diff --git a/client/src/sass/player/bezels.scss b/client/src/sass/player/bezels.scss
index ff3448511..853a030a3 100644
--- a/client/src/sass/player/bezels.scss
+++ b/client/src/sass/player/bezels.scss
@@ -32,11 +32,3 @@
32 fill: #fff; 32 fill: #fff;
33 } 33 }
34} 34}
35
36.video-js {
37
38 .vjs-bezel-content {
39
40 }
41
42}
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 41e2a535c..f80106428 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -21,6 +21,12 @@ body {
21 21
22 .vjs-dock-text { 22 .vjs-dock-text {
23 padding-right: 10px; 23 padding-right: 10px;
24 background: linear-gradient(to bottom, rgba(0, 0, 0, .6) 0, rgba(0, 0, 0, 0.2) 70%, rgba(0, 0, 0, 0) 100%);
25 }
26
27 .vjs-dock-title,
28 .vjs-dock-description {
29 text-shadow: 0 0 2px rgba(0, 0, 0, .5);
24 } 30 }
25 31
26 .vjs-dock-description { 32 .vjs-dock-description {
@@ -55,7 +61,7 @@ body {
55 $big-play-width: 1.2em; 61 $big-play-width: 1.2em;
56 $big-play-height: 1.2em; 62 $big-play-height: 1.2em;
57 63
58 border: 4px solid #fff; 64 border: 2px solid #fff;
59 border-radius: 100%; 65 border-radius: 100%;
60 66
61 left: 50%; 67 left: 50%;
@@ -185,7 +191,6 @@ body {
185 191
186 .vjs-play-progress { 192 .vjs-play-progress {
187 background: var(--embedForegroundColor); 193 background: var(--embedForegroundColor);
188 transition: all .2s ease 0s;
189 194
190 // Not display the circle if the progress is not hovered 195 // Not display the circle if the progress is not hovered
191 &::before { 196 &::before {
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index c91ae08b9..d5b42a025 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -1,15 +1,11 @@
1import './embed.scss' 1import './embed.scss'
2 2
3import { 3import {
4 getCompleteLocale,
5 is18nLocale,
6 isDefaultLocale,
7 peertubeTranslate, 4 peertubeTranslate,
8 ResultList, 5 ResultList,
9 ServerConfig, 6 ServerConfig,
10 VideoDetails 7 VideoDetails
11} from '../../../../shared' 8} from '../../../../shared'
12import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
13import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 9import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
14import { 10import {
15 P2PMediaLoaderOptions, 11 P2PMediaLoaderOptions,
@@ -19,10 +15,14 @@ import {
19import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' 15import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
20import { PeerTubeEmbedApi } from './embed-api' 16import { PeerTubeEmbedApi } from './embed-api'
21import { TranslationsManager } from '../../assets/player/translations-manager' 17import { TranslationsManager } from '../../assets/player/translations-manager'
18import { VideoJsPlayer } from 'video.js'
19import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
20
21type Translations = { [ id: string ]: string }
22 22
23export class PeerTubeEmbed { 23export class PeerTubeEmbed {
24 videoElement: HTMLVideoElement 24 videoElement: HTMLVideoElement
25 player: any 25 player: VideoJsPlayer
26 api: PeerTubeEmbedApi = null 26 api: PeerTubeEmbedApi = null
27 autoplay: boolean 27 autoplay: boolean
28 controls: boolean 28 controls: boolean
@@ -71,7 +71,7 @@ export class PeerTubeEmbed {
71 element.parentElement.removeChild(element) 71 element.parentElement.removeChild(element)
72 } 72 }
73 73
74 displayError (text: string, translations?: { [ id: string ]: string }) { 74 displayError (text: string, translations?: Translations) {
75 // Remove video element 75 // Remove video element
76 if (this.videoElement) this.removeElement(this.videoElement) 76 if (this.videoElement) this.removeElement(this.videoElement)
77 77
@@ -90,12 +90,12 @@ export class PeerTubeEmbed {
90 errorText.innerHTML = translatedText 90 errorText.innerHTML = translatedText
91 } 91 }
92 92
93 videoNotFound (translations?: { [ id: string ]: string }) { 93 videoNotFound (translations?: Translations) {
94 const text = 'This video does not exist.' 94 const text = 'This video does not exist.'
95 this.displayError(text, translations) 95 this.displayError(text, translations)
96 } 96 }
97 97
98 videoFetchError (translations?: { [ id: string ]: string }) { 98 videoFetchError (translations?: Translations) {
99 const text = 'We cannot fetch the video. Please try again later.' 99 const text = 'We cannot fetch the video. Please try again later.'
100 this.displayError(text, translations) 100 this.displayError(text, translations)
101 } 101 }
@@ -237,7 +237,7 @@ export class PeerTubeEmbed {
237 }) 237 })
238 } 238 }
239 239
240 this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: any) => this.player = player) 240 this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: VideoJsPlayer) => this.player = player)
241 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) 241 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
242 242
243 window[ 'videojsPlayer' ] = this.player 243 window[ 'videojsPlayer' ] = this.player
@@ -261,19 +261,19 @@ export class PeerTubeEmbed {
261 } 261 }
262 262
263 private async buildDock (videoInfo: VideoDetails, configResponse: Response) { 263 private async buildDock (videoInfo: VideoDetails, configResponse: Response) {
264 if (this.controls) { 264 if (!this.controls) return
265 const title = this.title ? videoInfo.name : undefined
266 265
267 const config: ServerConfig = await configResponse.json() 266 const title = this.title ? videoInfo.name : undefined
268 const description = config.tracker.enabled && this.warningTitle
269 ? '<span class="text">' + this.player.localize('Watching this video may reveal your IP address to others.') + '</span>'
270 : undefined
271 267
272 this.player.dock({ 268 const config: ServerConfig = await configResponse.json()
273 title, 269 const description = config.tracker.enabled && this.warningTitle
274 description 270 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
275 }) 271 : undefined
276 } 272
273 this.player.dock({
274 title,
275 description
276 })
277 } 277 }
278 278
279 private buildCSS () { 279 private buildCSS () {
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 8824c4f7c..c4f2d6a6a 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -26,7 +26,7 @@
26 "paths": { 26 "paths": {
27 "@app/*": [ "src/app/*" ], 27 "@app/*": [ "src/app/*" ],
28 "@shared/*": [ "../shared/*" ], 28 "@shared/*": [ "../shared/*" ],
29 "video.js": [ "node_modules/video.js/dist/alt/video.core.js" ], 29 "video.js": [ "node_modules/video.js/dist/alt/video.core.novtt" ],
30 "fs": [ "src/shims/noop" ], 30 "fs": [ "src/shims/noop" ],
31 "http": [ "src/shims/http" ], 31 "http": [ "src/shims/http" ],
32 "https": [ "src/shims/https" ], 32 "https": [ "src/shims/https" ],
diff --git a/client/webpack/webpack.video-embed.js b/client/webpack/webpack.video-embed.js
index 909048cca..f6d532556 100644
--- a/client/webpack/webpack.video-embed.js
+++ b/client/webpack/webpack.video-embed.js
@@ -27,7 +27,7 @@ module.exports = function () {
27 modules: [ helpers.root('src'), helpers.root('node_modules') ], 27 modules: [ helpers.root('src'), helpers.root('node_modules') ],
28 28
29 alias: { 29 alias: {
30 'video.js$': path.resolve('node_modules/video.js/dist/alt/video.core.js') 30 'video.js$': path.resolve('node_modules/video.js/dist/alt/video.core.novtt.js')
31 } 31 }
32 }, 32 },
33 33
diff --git a/client/yarn.lock b/client/yarn.lock
index 0855a2570..1b9c2cef1 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -987,11 +987,6 @@
987 semver "6.3.0" 987 semver "6.3.0"
988 semver-intersect "1.4.0" 988 semver-intersect "1.4.0"
989 989
990"@streamroot/videojs-hlsjs-plugin@^1.0.10":
991 version "1.0.13"
992 resolved "https://registry.yarnpkg.com/@streamroot/videojs-hlsjs-plugin/-/videojs-hlsjs-plugin-1.0.13.tgz#ae3afb3a5a3cd90e7b424b6b4cb14de1cde40836"
993 integrity sha512-A55213sFj8nuoj23YiR0r73cRV4dlnSwXGwT1Qiu+oqhsauhqN+lHSRHFztMIU4EMf2Cafvv5P4R+A2c/Uj6nw==
994
995"@types/bittorrent-protocol@*": 990"@types/bittorrent-protocol@*":
996 version "2.2.4" 991 version "2.2.4"
997 resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.4.tgz#7dc0716924bc6a904753d39846ad235c7dab4641" 992 resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.4.tgz#7dc0716924bc6a904753d39846ad235c7dab4641"
@@ -1045,6 +1040,11 @@
1045 resolved "https://registry.yarnpkg.com/@types/jschannel/-/jschannel-1.0.1.tgz#79d582ccf42554c8457230526a3054d018d559f0" 1040 resolved "https://registry.yarnpkg.com/@types/jschannel/-/jschannel-1.0.1.tgz#79d582ccf42554c8457230526a3054d018d559f0"
1046 integrity sha512-S34NuOoOOKXbft3f9GDeLKp777ABCGArZaqUWOuu1Xn+1S75Osmk8kCeqmw5x2TuASyjE082DwDAuoaXNIRCTw== 1041 integrity sha512-S34NuOoOOKXbft3f9GDeLKp777ABCGArZaqUWOuu1Xn+1S75Osmk8kCeqmw5x2TuASyjE082DwDAuoaXNIRCTw==
1047 1042
1043"@types/linkify-it@*":
1044 version "2.1.0"
1045 resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
1046 integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
1047
1048"@types/linkifyjs@^2.1.2": 1048"@types/linkifyjs@^2.1.2":
1049 version "2.1.2" 1049 version "2.1.2"
1050 resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.2.tgz#8244f4e6d7be65359cc25a34da8977fce87a7b2e" 1050 resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.2.tgz#8244f4e6d7be65359cc25a34da8977fce87a7b2e"
@@ -1071,10 +1071,12 @@
1071 dependencies: 1071 dependencies:
1072 "@types/node" "*" 1072 "@types/node" "*"
1073 1073
1074"@types/markdown-it@^0.0.5": 1074"@types/markdown-it@^0.0.9":
1075 version "0.0.5" 1075 version "0.0.9"
1076 resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.5.tgz#5cdcbe08e81075d5dbf15466b311359b02a30c2b" 1076 resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.9.tgz#a5d552f95216c478e0a27a5acc1b28dcffd989ce"
1077 integrity sha512-Bhc4jTJ3g+WU+dBvyhwwssHifjqapauyjV+0cTWVWRjwDAaK9PebZBFpLJmoOCp47qlkDeeT1Y9sV9LyyaG02w== 1077 integrity sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==
1078 dependencies:
1079 "@types/linkify-it" "*"
1078 1080
1079"@types/minimatch@*": 1081"@types/minimatch@*":
1080 version "3.0.3" 1082 version "3.0.3"
@@ -1157,10 +1159,10 @@
1157 resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" 1159 resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
1158 integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== 1160 integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
1159 1161
1160"@types/video.js@^7.2.5": 1162"@types/video.js@^7.3.3":
1161 version "7.2.15" 1163 version "7.3.3"
1162 resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.2.15.tgz#03d950f01c985a5082ead4d1b73064455a1c8c6f" 1164 resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.3.tgz#b6870d954473dfd694e10b55a90c0f3be8522da3"
1163 integrity sha512-NsojVfvTwdVqDe0+vJaoHOO2iuLm0sp/u8jEsZeLGsM3gNfg5WIOFd6NC0cQR9JHUuDPPSPF70jxdklGWm5jhQ== 1165 integrity sha512-yAb46+4A0dKFxOQRVLoLyfC/S/BmHLE10MxPXt/t88+7R4GWLHosHelVtYpKBRykjptdkqfQXNRXoQzDeKm6MA==
1164 1166
1165"@types/webpack-sources@^0.1.5": 1167"@types/webpack-sources@^0.1.5":
1166 version "0.1.5" 1168 version "0.1.5"
@@ -2586,6 +2588,29 @@ chardet@^0.7.0:
2586 resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 2588 resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
2587 integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== 2589 integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
2588 2590
2591chart.js@^2.9.3:
2592 version "2.9.3"
2593 resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7"
2594 integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==
2595 dependencies:
2596 chartjs-color "^2.1.0"
2597 moment "^2.10.2"
2598
2599chartjs-color-string@^0.6.0:
2600 version "0.6.0"
2601 resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
2602 integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
2603 dependencies:
2604 color-name "^1.0.0"
2605
2606chartjs-color@^2.1.0:
2607 version "2.4.1"
2608 resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
2609 integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
2610 dependencies:
2611 chartjs-color-string "^0.6.0"
2612 color-convert "^1.9.3"
2613
2589check-types@^8.0.3: 2614check-types@^8.0.3:
2590 version "8.0.3" 2615 version "8.0.3"
2591 resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" 2616 resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
@@ -2800,7 +2825,7 @@ collection-visit@^1.0.0:
2800 map-visit "^1.0.0" 2825 map-visit "^1.0.0"
2801 object-visit "^1.0.0" 2826 object-visit "^1.0.0"
2802 2827
2803color-convert@^1.9.0: 2828color-convert@^1.9.0, color-convert@^1.9.3:
2804 version "1.9.3" 2829 version "1.9.3"
2805 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 2830 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
2806 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 2831 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -2812,6 +2837,11 @@ color-name@1.1.3:
2812 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 2837 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
2813 integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 2838 integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
2814 2839
2840color-name@^1.0.0:
2841 version "1.1.4"
2842 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
2843 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
2844
2815colors@1.1.2: 2845colors@1.1.2:
2816 version "1.1.2" 2846 version "1.1.2"
2817 resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 2847 resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -6941,6 +6971,11 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
6941 dependencies: 6971 dependencies:
6942 minimist "0.0.8" 6972 minimist "0.0.8"
6943 6973
6974moment@^2.10.2:
6975 version "2.24.0"
6976 resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
6977 integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
6978
6944mousetrap@^1.6.0: 6979mousetrap@^1.6.0:
6945 version "1.6.3" 6980 version "1.6.3"
6946 resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" 6981 resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a"
@@ -10809,6 +10844,11 @@ void-elements@^2.0.0:
10809 resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" 10844 resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
10810 integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= 10845 integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
10811 10846
10847vtt.js@^0.13.0:
10848 version "0.13.0"
10849 resolved "https://registry.yarnpkg.com/vtt.js/-/vtt.js-0.13.0.tgz#955c667b34d5325b2012cb9e8ba9bad6e0b11ff8"
10850 integrity sha1-lVxmezTVMlsgEsuei6m61uCxH/g=
10851
10812watchpack@^1.6.0: 10852watchpack@^1.6.0:
10813 version "1.6.0" 10853 version "1.6.0"
10814 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" 10854 resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"