diff options
246 files changed, 5025 insertions, 2555 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b733e4b6..13bec7535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -1,10 +1,99 @@ | |||
1 | # Changelog | 1 | # Changelog |
2 | 2 | ||
3 | ## v1.2.0 | ||
4 | |||
5 | ### BREAKING CHANGES | ||
6 | |||
7 | * **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir)) | ||
8 | * **Docker:** Check you have all the storage fields in your `/config/production.yaml` file: https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/production.yaml#L34 | ||
9 | * **nginx:** Add redundancy endpoint in static file. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx** | ||
10 | * **nginx:** Add socket io endpoint. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx** | ||
11 | * Moderators can manage users now (add/delete/update/block) | ||
12 | * Add `tmp` and `redundancy` directories in configuration file. **You should configure them in your production.yaml** | ||
13 | |||
14 | ### Maintenance | ||
15 | |||
16 | * Check free storage before upgrading in upgrade script ([@Nutomic](https://github.com/nutomic)) | ||
17 | * Explain that PeerTube must be stopped in prune storage script | ||
18 | * Add some security directives in the systemd unit configuration file ([@rigelk](https://github.com/rigelk) & [@mkoppmann](https://github.com/mkoppmann)) | ||
19 | * Update FreeBSD startup script ([@gegeweb](https://github.com/gegeweb)) | ||
20 | |||
21 | ### Docker | ||
22 | |||
23 | * Patch docker entrypoint to speed up the chown at startup ([LecygneNoir](https://github.com/LecygneNoir)) | ||
24 | |||
25 | ### Features | ||
26 | |||
27 | * Add Russian, Polish and Italian languages | ||
28 | * Add user notifications: | ||
29 | * Notification types: | ||
30 | * Comment on my video | ||
31 | * New video from my subscriptions | ||
32 | * New video abuses (for moderators) | ||
33 | * Blacklist/Unblacklist on my video | ||
34 | * Video import finished (error or success) | ||
35 | * Pending video published (after transcoding or a scheduled update) | ||
36 | * My account or one of my channel has a new follower | ||
37 | * Someone (except muted accounts) mentioned me in comments | ||
38 | * A user registered on the instance (for moderators) | ||
39 | * Notification actions: | ||
40 | * Add a web notification | ||
41 | * Send an english email | ||
42 | * Add contact form in about page (**enabled by default**) | ||
43 | * Add ability to unfederate a local video in blacklist modal (**checkbox checked by default**) | ||
44 | * Support additional video extensions if transcoding is enabled (**enabled by default**) | ||
45 | * Redirect to the last url on login | ||
46 | * Add ability to automatically set the video caption in URL. Example: https://peertube2.cpy.re/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d?subtitle=ru | ||
47 | * Automatically enable the last selected caption when watching a video | ||
48 | * Add ability to disable, clear and list user videos history | ||
49 | * Add a button to help to translate peertube | ||
50 | * Add text in the report modal to explain to whom the report will be sent | ||
51 | * Open my account menu entries on hover | ||
52 | * Explain what features are enabled on the instance in the about page | ||
53 | * Add an error message in the forgot password modal if the instance email system is not configured | ||
54 | * Add sitemap | ||
55 | * Add well known url to change password ([@rigelk](https://github.com/rigelk)) | ||
56 | * Remove 8GB video upload limit on client side. There may still be such limit depending on the reverse proxy configuration ([@scanlime](https://github.com/scanlime)) | ||
57 | * Add CSP ([@rigelk](https://github.com/rigelk) & [@Nutomic](https://github.com/nutomic)) | ||
58 | * Update title and description HTML tags when rendering video HTML page | ||
59 | * Add webfinger support for remote follows ([@acid-chicken](https://github.com/acid-chicken)) | ||
60 | * Add tooltip to explain how the trending algorithm works ([@auberanger](https://github.com/auberanger)) | ||
61 | * Warn users when they want to delete a channel because they will not be able to create another channel with the same name | ||
62 | * Warn users when they leave the video upload/update (on page refresh/tab close) | ||
63 | * Set max user name, user display name, channel name and channel display name lengths to 50 characters ([@McFlat](https://github.com/mcflat)) | ||
64 | * Increase video abuse length to 3000 characters | ||
65 | * Add totalLocalVideoFilesSize in the stats endpoint | ||
66 | |||
67 | ## Bug fixes | ||
68 | |||
69 | * Fix the addition of captions to a video | ||
70 | * Fix federation of some videos | ||
71 | * Fix NSFW blur on search | ||
72 | * Add error message when trying to upload .ass subtitles | ||
73 | * Fix default homepage in the progressive web application | ||
74 | * Don't crash on queue error | ||
75 | * Fix EXDEV errors if you have multiple mount points | ||
76 | * Fix broken audio in transcoding with some videos | ||
77 | * Fix crash on getVideoFileStream issue | ||
78 | * Fix followers search | ||
79 | * Remove trailing `/` in CLI import script ([@HesioZ](https://github.com/HesioZ/)) | ||
80 | * Use origin video url in canonical tag | ||
81 | * Fix captions in HTTP fallback | ||
82 | * Automatically refresh remote actors to fix deleted remote actors that are still displayed on some instances | ||
83 | * Add missing translations in video embed page | ||
84 | * Fix some styling issues in dark mode | ||
85 | * Fix transcoding issues with some videos | ||
86 | * Fix Mac OS mkv/avi upload | ||
87 | * Fix menu overflow on mobile | ||
88 | * Fix ownership button icons ([@joshmorel](https://github.com/joshmorel)) | ||
89 | |||
90 | |||
3 | ## v1.1.0 | 91 | ## v1.1.0 |
4 | 92 | ||
5 | ***Since v1.0.1*** | 93 | ***Since v1.0.1*** |
6 | 94 | ||
7 | ### BREAKING CHANGES | 95 | ### BREAKING CHANGES |
96 | |||
8 | * **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir)) | 97 | * **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir)) |
9 | 98 | ||
10 | ### Maintenance | 99 | ### Maintenance |
@@ -133,7 +133,7 @@ You can also join the cheerful bunch that makes our community: | |||
133 | 133 | ||
134 | * Chat<a name="contact"></a>: | 134 | * Chat<a name="contact"></a>: |
135 | * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** | 135 | * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** |
136 | * Matrix (bridged on the IRC channel) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)** | 136 | * Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)** |
137 | * Forum: | 137 | * Forum: |
138 | * Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube) | 138 | * Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube) |
139 | 139 | ||
diff --git a/client/package.json b/client/package.json index 5fe1f3d5f..3eea661f1 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -1,6 +1,6 @@ | |||
1 | { | 1 | { |
2 | "name": "peertube-client", | 2 | "name": "peertube-client", |
3 | "version": "1.1.0", | 3 | "version": "1.2.0", |
4 | "private": true, | 4 | "private": true, |
5 | "licence": "GPLv3", | 5 | "licence": "GPLv3", |
6 | "author": { | 6 | "author": { |
@@ -28,7 +28,8 @@ | |||
28 | "resolutions": { | 28 | "resolutions": { |
29 | "video.js": "^7", | 29 | "video.js": "^7", |
30 | "webtorrent/create-torrent/junk": "^1", | 30 | "webtorrent/create-torrent/junk": "^1", |
31 | "simple-get": "^2.8.1" | 31 | "simple-get": "^2.8.1", |
32 | "punycode": "^1.4.1" | ||
32 | }, | 33 | }, |
33 | "jest": { | 34 | "jest": { |
34 | "globals": { | 35 | "globals": { |
@@ -63,20 +64,20 @@ | |||
63 | "setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts" | 64 | "setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts" |
64 | }, | 65 | }, |
65 | "devDependencies": { | 66 | "devDependencies": { |
66 | "@angular-devkit/build-angular": "~0.11.1", | 67 | "@angular-devkit/build-angular": "~0.13.1", |
67 | "@angular/animations": "~7.1.1", | 68 | "@angular/animations": "~7.2.4", |
68 | "@angular/cli": "~7.1.1", | 69 | "@angular/cli": "~7.3.1", |
69 | "@angular/common": "~7.1.1", | 70 | "@angular/common": "~7.2.4", |
70 | "@angular/compiler": "~7.1.1", | 71 | "@angular/compiler": "~7.2.4", |
71 | "@angular/compiler-cli": "~7.1.1", | 72 | "@angular/compiler-cli": "~7.2.4", |
72 | "@angular/core": "~7.1.1", | 73 | "@angular/core": "~7.2.4", |
73 | "@angular/forms": "~7.1.1", | 74 | "@angular/forms": "~7.2.4", |
74 | "@angular/http": "~7.1.1", | 75 | "@angular/http": "~7.2.4", |
75 | "@angular/language-service": "~7.1.1", | 76 | "@angular/language-service": "~7.2.4", |
76 | "@angular/platform-browser": "~7.1.1", | 77 | "@angular/platform-browser": "~7.2.4", |
77 | "@angular/platform-browser-dynamic": "~7.1.1", | 78 | "@angular/platform-browser-dynamic": "~7.2.4", |
78 | "@angular/router": "~7.1.1", | 79 | "@angular/router": "~7.2.4", |
79 | "@angular/service-worker": "~7.1.1", | 80 | "@angular/service-worker": "~7.2.4", |
80 | "@angularclass/hmr": "^2.1.3", | 81 | "@angularclass/hmr": "^2.1.3", |
81 | "@neos21/bootstrap3-glyphicons": "^1.0.1", | 82 | "@neos21/bootstrap3-glyphicons": "^1.0.1", |
82 | "@ng-bootstrap/ng-bootstrap": "^4.0.0", | 83 | "@ng-bootstrap/ng-bootstrap": "^4.0.0", |
@@ -85,7 +86,9 @@ | |||
85 | "@ngx-loading-bar/router": "^3.0.0", | 86 | "@ngx-loading-bar/router": "^3.0.0", |
86 | "@ngx-meta/core": "^6.0.0-rc.1", | 87 | "@ngx-meta/core": "^6.0.0-rc.1", |
87 | "@ngx-translate/i18n-polyfill": "^1.0.0", | 88 | "@ngx-translate/i18n-polyfill": "^1.0.0", |
89 | "@streamroot/videojs-hlsjs-plugin": "^1.0.7", | ||
88 | "@types/core-js": "^2.5.0", | 90 | "@types/core-js": "^2.5.0", |
91 | "@types/hls.js": "^0.12.0", | ||
89 | "@types/jasmine": "^2.8.7", | 92 | "@types/jasmine": "^2.8.7", |
90 | "@types/jasminewd2": "^2.0.3", | 93 | "@types/jasminewd2": "^2.0.3", |
91 | "@types/jest": "^23.3.1", | 94 | "@types/jest": "^23.3.1", |
@@ -109,6 +112,7 @@ | |||
109 | "extract-text-webpack-plugin": "4.0.0-beta.0", | 112 | "extract-text-webpack-plugin": "4.0.0-beta.0", |
110 | "file-loader": "^2.0.0", | 113 | "file-loader": "^2.0.0", |
111 | "focus-visible": "^4.1.5", | 114 | "focus-visible": "^4.1.5", |
115 | "hls.js": "^0.12.2", | ||
112 | "html-loader": "^0.5.5", | 116 | "html-loader": "^0.5.5", |
113 | "html-webpack-plugin": "^3.2.0", | 117 | "html-webpack-plugin": "^3.2.0", |
114 | "https-browserify": "^1.0.0", | 118 | "https-browserify": "^1.0.0", |
@@ -131,6 +135,7 @@ | |||
131 | "ngx-qrcode2": "^0.0.9", | 135 | "ngx-qrcode2": "^0.0.9", |
132 | "node-sass": "^4.9.3", | 136 | "node-sass": "^4.9.3", |
133 | "npm-font-source-sans-pro": "^1.0.2", | 137 | "npm-font-source-sans-pro": "^1.0.2", |
138 | "p2p-media-loader-hlsjs": "^0.4.0", | ||
134 | "path-browserify": "^1.0.0", | 139 | "path-browserify": "^1.0.0", |
135 | "primeng": "^7.0.0", | 140 | "primeng": "^7.0.0", |
136 | "process": "^0.11.10", | 141 | "process": "^0.11.10", |
@@ -152,9 +157,9 @@ | |||
152 | "typescript": "3.1.6", | 157 | "typescript": "3.1.6", |
153 | "video.js": "^7", | 158 | "video.js": "^7", |
154 | "videojs-contextmenu-ui": "^5.0.0", | 159 | "videojs-contextmenu-ui": "^5.0.0", |
160 | "videojs-contrib-quality-levels": "^2.0.9", | ||
155 | "videojs-dock": "^2.0.2", | 161 | "videojs-dock": "^2.0.2", |
156 | "videojs-hotkeys": "^0.2.21", | 162 | "videojs-hotkeys": "^0.2.21", |
157 | "webpack": "^4.17.1", | ||
158 | "webpack-bundle-analyzer": "^3.0.2", | 163 | "webpack-bundle-analyzer": "^3.0.2", |
159 | "webpack-cli": "^3.0.8", | 164 | "webpack-cli": "^3.0.8", |
160 | "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d", | 165 | "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d", |
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html index 2b3fb32f3..b2cbd0873 100644 --- a/client/src/app/+about/about-instance/contact-admin-modal.component.html +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4> | 3 | <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index 036264602..e8339b78b 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -26,8 +26,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
26 | private notifier: Notifier, | 26 | private notifier: Notifier, |
27 | private restExtractor: RestExtractor, | 27 | private restExtractor: RestExtractor, |
28 | private redirectService: RedirectService, | 28 | private redirectService: RedirectService, |
29 | private authService: AuthService, | 29 | private authService: AuthService |
30 | private i18n: I18n | ||
31 | ) {} | 30 | ) {} |
32 | 31 | ||
33 | ngOnInit () { | 32 | ngOnInit () { |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index c06ae1d60..f7f347105 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list. | |||
10 | import { JobsComponent } from './jobs/job.component' | 10 | import { JobsComponent } from './jobs/job.component' |
11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' | 11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' |
12 | import { JobService } from './jobs/shared/job.service' | 12 | import { JobService } from './jobs/shared/job.service' |
13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' | 13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' |
14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' | 14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' |
15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
@@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f | |||
36 | UsersComponent, | 36 | UsersComponent, |
37 | UserCreateComponent, | 37 | UserCreateComponent, |
38 | UserUpdateComponent, | 38 | UserUpdateComponent, |
39 | UserPasswordComponent, | ||
39 | UserListComponent, | 40 | UserListComponent, |
40 | 41 | ||
41 | ModerationComponent, | 42 | ModerationComponent, |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html index 952235c55..303a788d2 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Moderation comment</h4> | 3 | <h4 i18n class="modal-title">Moderation comment</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -19,7 +20,7 @@ | |||
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Update this comment" class="action-button-submit" | 26 | type="submit" i18n-value value="Update this comment" class="action-button-submit" |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts index bebcb4207..f915978ee 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts | |||
@@ -45,7 +45,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
45 | }) | 45 | }) |
46 | } | 46 | } |
47 | 47 | ||
48 | hideModerationCommentModal () { | 48 | hide () { |
49 | this.abuseToComment = undefined | 49 | this.abuseToComment = undefined |
50 | this.openedModal.close() | 50 | this.openedModal.close() |
51 | this.form.reset() | 51 | this.form.reset() |
@@ -60,7 +60,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
60 | this.notifier.success(this.i18n('Comment updated.')) | 60 | this.notifier.success(this.i18n('Comment updated.')) |
61 | 61 | ||
62 | this.commentUpdated.emit(moderationComment) | 62 | this.commentUpdated.emit(moderationComment) |
63 | this.hideModerationCommentModal() | 63 | this.hide() |
64 | }, | 64 | }, |
65 | 65 | ||
66 | err => this.notifier.error(err.message) | 66 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts index fd80a02e0..ec734ef92 100644 --- a/client/src/app/+admin/users/user-edit/index.ts +++ b/client/src/app/+admin/users/user-edit/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './user-create.component' | 1 | export * from './user-create.component' |
2 | export * from './user-update.component' | 2 | export * from './user-update.component' |
3 | export * from './user-password.component' | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 56cf7d17d..c6566da24 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -81,3 +81,17 @@ | |||
81 | 81 | ||
82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> |
83 | </form> | 83 | </form> |
84 | |||
85 | <div *ngIf="!isCreation()" class="danger-zone"> | ||
86 | <div class="account-title" i18n>Danger Zone</div> | ||
87 | |||
88 | <div class="form-group reset-password-email"> | ||
89 | <label i18n>Send a link to reset the password by email to the user</label> | ||
90 | <button (click)="resetPassword()" i18n>Ask for new password</button> | ||
91 | </div> | ||
92 | |||
93 | <div class="form-group"> | ||
94 | <label i18n>Manually set the user password</label> | ||
95 | <my-user-password [userId]="userId"></my-user-password> | ||
96 | </div> | ||
97 | </div> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 6675f65cc..c1cc4ca45 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -14,7 +14,7 @@ input:not([type=submit]) { | |||
14 | @include peertube-select-container(340px); | 14 | @include peertube-select-container(340px); |
15 | } | 15 | } |
16 | 16 | ||
17 | input[type=submit] { | 17 | input[type=submit], button { |
18 | @include peertube-button; | 18 | @include peertube-button; |
19 | @include orange-button; | 19 | @include orange-button; |
20 | 20 | ||
@@ -25,3 +25,23 @@ input[type=submit] { | |||
25 | margin-top: 5px; | 25 | margin-top: 5px; |
26 | font-size: 11px; | 26 | font-size: 11px; |
27 | } | 27 | } |
28 | |||
29 | .account-title { | ||
30 | @include in-content-small-title; | ||
31 | |||
32 | margin-top: 55px; | ||
33 | margin-bottom: 30px; | ||
34 | } | ||
35 | |||
36 | .danger-zone { | ||
37 | .reset-password-email { | ||
38 | margin-bottom: 30px; | ||
39 | padding-bottom: 30px; | ||
40 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); | ||
41 | |||
42 | button { | ||
43 | display: block; | ||
44 | margin-top: 0; | ||
45 | } | ||
46 | } | ||
47 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 0b3511e8e..649b35b0c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive { | |||
8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
9 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) | 9 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) |
10 | username: string | 10 | username: string |
11 | userId: number | ||
11 | 12 | ||
12 | protected abstract serverService: ServerService | 13 | protected abstract serverService: ServerService |
13 | protected abstract configService: ConfigService | 14 | protected abstract configService: ConfigService |
@@ -22,7 +23,9 @@ export abstract class UserEdit extends FormReactive { | |||
22 | } | 23 | } |
23 | 24 | ||
24 | computeQuotaWithTranscoding () { | 25 | computeQuotaWithTranscoding () { |
25 | const resolutions = this.serverService.getConfig().transcoding.enabledResolutions | 26 | const transcodingConfig = this.serverService.getConfig().transcoding |
27 | |||
28 | const resolutions = transcodingConfig.enabledResolutions | ||
26 | const higherResolution = VideoResolution.H_1080P | 29 | const higherResolution = VideoResolution.H_1080P |
27 | let multiplier = 0 | 30 | let multiplier = 0 |
28 | 31 | ||
@@ -30,9 +33,15 @@ export abstract class UserEdit extends FormReactive { | |||
30 | multiplier += resolution / higherResolution | 33 | multiplier += resolution / higherResolution |
31 | } | 34 | } |
32 | 35 | ||
36 | if (transcodingConfig.hls.enabled) multiplier *= 2 | ||
37 | |||
33 | return multiplier * parseInt(this.form.value['videoQuota'], 10) | 38 | return multiplier * parseInt(this.form.value['videoQuota'], 10) |
34 | } | 39 | } |
35 | 40 | ||
41 | resetPassword () { | ||
42 | return | ||
43 | } | ||
44 | |||
36 | protected buildQuotaOptions () { | 45 | protected buildQuotaOptions () { |
37 | // These are used by a HTML select, so convert key into strings | 46 | // These are used by a HTML select, so convert key into strings |
38 | this.videoQuotaOptions = this.configService | 47 | this.videoQuotaOptions = this.configService |
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html new file mode 100644 index 000000000..a1e1f6216 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | ||
2 | <div class="form-group"> | ||
3 | |||
4 | <div class="input-group"> | ||
5 | <input id="password" [attr.type]="showPassword ? 'text' : 'password'" | ||
6 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
7 | > | ||
8 | <div class="input-group-append"> | ||
9 | <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> | ||
10 | <ng-container *ngIf="!showPassword" i18n>Show</ng-container> | ||
11 | <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> | ||
12 | </button> | ||
13 | </div> | ||
14 | </div> | ||
15 | <div *ngIf="formErrors.password" class="form-error"> | ||
16 | {{ formErrors.password }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
21 | </form> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..217d585af --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss | |||
@@ -0,0 +1,22 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input:not([type=submit]):not([type=checkbox]) { | ||
5 | @include peertube-input-text(340px); | ||
6 | |||
7 | display: block; | ||
8 | border-top-right-radius: 0; | ||
9 | border-bottom-right-radius: 0; | ||
10 | border-right: none; | ||
11 | } | ||
12 | |||
13 | input[type=submit] { | ||
14 | @include peertube-button; | ||
15 | @include orange-button; | ||
16 | |||
17 | margin-top: 10px; | ||
18 | } | ||
19 | |||
20 | .input-group-append { | ||
21 | height: 30px; | ||
22 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..5b3040440 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { UserService } from '@app/shared/users/user.service' | ||
4 | import { Notifier } from '../../../core' | ||
5 | import { User, UserUpdate } from '../../../../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
9 | import { FormReactive } from '../../../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-user-password', | ||
13 | templateUrl: './user-password.component.html', | ||
14 | styleUrls: [ './user-password.component.scss' ] | ||
15 | }) | ||
16 | export class UserPasswordComponent extends FormReactive implements OnInit { | ||
17 | error: string | ||
18 | username: string | ||
19 | showPassword = false | ||
20 | |||
21 | @Input() userId: number | ||
22 | |||
23 | constructor ( | ||
24 | protected formValidatorService: FormValidatorService, | ||
25 | private userValidatorsService: UserValidatorsService, | ||
26 | private route: ActivatedRoute, | ||
27 | private router: Router, | ||
28 | private notifier: Notifier, | ||
29 | private userService: UserService, | ||
30 | private i18n: I18n | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | password: this.userValidatorsService.USER_PASSWORD | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | formValidated () { | ||
42 | this.error = undefined | ||
43 | |||
44 | const userUpdate: UserUpdate = this.form.value | ||
45 | |||
46 | this.userService.updateUser(this.userId, userUpdate).subscribe( | ||
47 | () => { | ||
48 | this.notifier.success( | ||
49 | this.i18n('Password changed for user {{username}}.', { username: this.username }) | ||
50 | ) | ||
51 | }, | ||
52 | |||
53 | err => this.error = err.message | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | togglePasswordVisibility () { | ||
58 | this.showPassword = !this.showPassword | ||
59 | } | ||
60 | |||
61 | getFormButtonTitle () { | ||
62 | return this.i18n('Update user password') | ||
63 | } | ||
64 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index 61e641823..94ef87b08 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared' | |||
19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | 19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { |
20 | error: string | 20 | error: string |
21 | userId: number | 21 | userId: number |
22 | userEmail: string | ||
22 | username: string | 23 | username: string |
23 | 24 | ||
24 | private paramsSub: Subscription | 25 | private paramsSub: Subscription |
@@ -89,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
89 | return this.i18n('Update user') | 90 | return this.i18n('Update user') |
90 | } | 91 | } |
91 | 92 | ||
93 | resetPassword () { | ||
94 | this.userService.askResetPassword(this.userEmail).subscribe( | ||
95 | () => { | ||
96 | this.notifier.success( | ||
97 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) | ||
98 | ) | ||
99 | }, | ||
100 | |||
101 | err => this.error = err.message | ||
102 | ) | ||
103 | } | ||
104 | |||
92 | private onUserFetched (userJson: User) { | 105 | private onUserFetched (userJson: User) { |
93 | this.userId = userJson.id | 106 | this.userId = userJson.id |
94 | this.username = userJson.username | 107 | this.username = userJson.username |
108 | this.userEmail = userJson.email | ||
95 | 109 | ||
96 | this.form.patchValue({ | 110 | this.form.patchValue({ |
97 | email: userJson.email, | 111 | email: userJson.email, |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 8c03a924b..69a4616a3 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div i18n class="form-sub-title">Users list</div> | 2 | <div i18n class="form-sub-title">Users list</div> |
3 | 3 | ||
4 | <a class="add-button" routerLink="/admin/users/create"> | 4 | <a class="add-button" routerLink="/admin/users/create"> |
5 | <span class="icon icon-add"></span> | 5 | <my-global-icon iconName="add"></my-global-icon> |
6 | <ng-container i18n>Create user</ng-container> | 6 | <ng-container i18n>Create user</ng-container> |
7 | </a> | 7 | </a> |
8 | </div> | 8 | </div> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index f235769f0..5274be01c 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .add-button { | 4 | .add-button { |
5 | @include create-button('../../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | tr.banned { | 8 | tr.banned { |
@@ -23,4 +23,4 @@ tr.banned { | |||
23 | input { | 23 | input { |
24 | @include peertube-input-text(250px); | 24 | @include peertube-input-text(250px); |
25 | } | 25 | } |
26 | } \ No newline at end of file | 26 | } |
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss index 82150cbe3..e7c6863f1 100644 --- a/client/src/app/+my-account/my-account-history/my-account-history.component.scss +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss | |||
@@ -65,10 +65,10 @@ | |||
65 | text-overflow: ellipsis; | 65 | text-overflow: ellipsis; |
66 | white-space: nowrap; | 66 | white-space: nowrap; |
67 | font-size: 14px; | 67 | font-size: 14px; |
68 | color: #585858; | 68 | color: $grey-foreground-color; |
69 | 69 | ||
70 | &:hover { | 70 | &:hover { |
71 | color: #303030; | 71 | color: $grey-foreground-hover-color; |
72 | } | 72 | } |
73 | } | 73 | } |
74 | } | 74 | } |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index b98a1087e..d518b22ec 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -1,7 +1,13 @@ | |||
1 | <div class="header"> | 1 | <div class="header"> |
2 | <a routerLink="/my-account/settings" fragment="notifications" i18n>Notification preferences</a> | 2 | <a routerLink="/my-account/settings" fragment="notifications" i18n> |
3 | <my-global-icon iconName="cog"></my-global-icon> | ||
4 | Notification preferences | ||
5 | </a> | ||
3 | 6 | ||
4 | <button (click)="markAllAsRead()" i18n>Mark all as read</button> | 7 | <button (click)="markAllAsRead()" i18n> |
8 | <my-global-icon iconName="circle-tick"></my-global-icon> | ||
9 | Mark all as read | ||
10 | </button> | ||
5 | </div> | 11 | </div> |
6 | 12 | ||
7 | <my-user-notifications #userNotification></my-user-notifications> | 13 | <my-user-notifications #userNotification></my-user-notifications> |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss index 86ac094c5..43d1f82ab 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss | |||
@@ -5,16 +5,18 @@ | |||
5 | display: flex; | 5 | display: flex; |
6 | justify-content: space-between; | 6 | justify-content: space-between; |
7 | font-size: 15px; | 7 | font-size: 15px; |
8 | margin-bottom: 10px; | 8 | margin-bottom: 20px; |
9 | 9 | ||
10 | a { | 10 | a { |
11 | @include peertube-button-link; | 11 | @include peertube-button-link; |
12 | @include grey-button; | 12 | @include grey-button; |
13 | @include button-with-icon(18px, 3px, -1px); | ||
13 | } | 14 | } |
14 | 15 | ||
15 | button { | 16 | button { |
16 | @include peertube-button; | 17 | @include peertube-button; |
17 | @include grey-button; | 18 | @include grey-button; |
19 | @include button-with-icon(20px, 3px, -1px); | ||
18 | } | 20 | } |
19 | } | 21 | } |
20 | 22 | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html index fd7d7d23b..674a4e8a2 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Accept ownership</h4> | 3 | <h4 i18n class="modal-title">Accept ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html index 379fd8bb1..5709e9f54 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html | |||
@@ -40,10 +40,10 @@ | |||
40 | <td class="action-cell"> | 40 | <td class="action-cell"> |
41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | 41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> |
42 | <my-button i18n label="Accept" | 42 | <my-button i18n label="Accept" |
43 | icon="icon-tick" | 43 | icon="tick" |
44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> | 44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> |
45 | <my-button i18n label="Refuse" | 45 | <my-button i18n label="Refuse" |
46 | icon="icon-cross" | 46 | icon="cross" |
47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> | 47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> |
48 | </ng-container> | 48 | </ng-container> |
49 | </td> | 49 | </td> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index df74b19b6..51db2e75d 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div class="video-channels-header"> | 1 | <div class="video-channels-header"> |
2 | <a class="create-button" routerLink="create"> | 2 | <a class="create-button" routerLink="create"> |
3 | <span class="icon icon-add"></span> | 3 | <my-global-icon iconName="add"></my-global-icon> |
4 | <ng-container i18n>Create another video channel</ng-container> | 4 | <ng-container i18n>Create another video channel</ng-container> |
5 | </a> | 5 | </a> |
6 | </div> | 6 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index 472cbb723..77fce138b 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .create-button { | 4 | .create-button { |
5 | @include create-button('../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | /deep/ .action-button { | 8 | /deep/ .action-button { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index a6911e4bf..69748ef37 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -32,7 +32,7 @@ | |||
32 | </span> | 32 | </span> |
33 | 33 | ||
34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | 34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> |
35 | <span class="icon icon-delete-white"></span> | 35 | <my-global-icon iconName="delete"></my-global-icon> |
36 | <ng-container i18n>Delete</ng-container> | 36 | <ng-container i18n>Delete</ng-container> |
37 | </span> | 37 | </span> |
38 | </div> | 38 | </div> |
@@ -45,7 +45,7 @@ | |||
45 | 45 | ||
46 | <my-button i18n-label label="Change ownership" | 46 | <my-button i18n-label label="Change ownership" |
47 | className="action-button-change-ownership" | 47 | className="action-button-change-ownership" |
48 | icon="icon-im-with-her" | 48 | icon="im-with-her" |
49 | (click)="changeOwnership($event, video)" | 49 | (click)="changeOwnership($event, video)" |
50 | ></my-button> | 50 | ></my-button> |
51 | </div> | 51 | </div> |
@@ -53,4 +53,4 @@ | |||
53 | </div> | 53 | </div> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> \ No newline at end of file | 56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index a735562f8..39d0cf2f7 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss | |||
@@ -23,14 +23,11 @@ | |||
23 | .action-button-delete-selection { | 23 | .action-button-delete-selection { |
24 | @include peertube-button; | 24 | @include peertube-button; |
25 | @include orange-button; | 25 | @include orange-button; |
26 | } | 26 | @include button-with-icon(21px); |
27 | |||
28 | .icon.icon-delete-white { | ||
29 | @include icon(21px); | ||
30 | 27 | ||
31 | position: relative; | 28 | my-global-icon { |
32 | top: -2px; | 29 | @include apply-svg-color(#fff); |
33 | background-image: url('../../../assets/images/global/delete-white.svg'); | 30 | } |
34 | } | 31 | } |
35 | } | 32 | } |
36 | } | 33 | } |
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html index 7c0df850d..22f127904 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change ownership</h4> | 3 | <h4 i18n class="modal-title">Change ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 3a60139e1..d398d4f35 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -30,14 +30,16 @@ | |||
30 | 30 | ||
31 | <footer class="row"> | 31 | <footer class="row"> |
32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - | 32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - |
33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a> | 33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a> |
34 | </footer> | 34 | </footer> |
35 | </div> | 35 | </div> |
36 | </div> | 36 | </div> |
37 | </div> | 37 | </div> |
38 | 38 | ||
39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> | 39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> |
40 | |||
40 | <my-confirm></my-confirm> | 41 | <my-confirm></my-confirm> |
42 | |||
41 | <p-toast position="bottom-right"> | 43 | <p-toast position="bottom-right"> |
42 | <ng-template let-message pTemplate="message"> | 44 | <ng-template let-message pTemplate="message"> |
43 | <div class="notification-block"> | 45 | <div class="notification-block"> |
diff --git a/client/src/app/core/confirm/index.ts b/client/src/app/core/confirm/index.ts index 44aabfc13..aca591e1a 100644 --- a/client/src/app/core/confirm/index.ts +++ b/client/src/app/core/confirm/index.ts | |||
@@ -1,2 +1 @@ | |||
1 | export * from './confirm.component' | ||
2 | export * from './confirm.service' | export * from './confirm.service' | |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 3bc0e2885..4ef3b1e73 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -8,7 +8,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | |||
8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' | 8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' |
9 | 9 | ||
10 | import { AuthService } from './auth' | 10 | import { AuthService } from './auth' |
11 | import { ConfirmComponent, ConfirmService } from './confirm' | 11 | import { ConfirmService } from './confirm' |
12 | import { throwIfAlreadyLoaded } from './module-import-guard' | 12 | import { throwIfAlreadyLoaded } from './module-import-guard' |
13 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' | 13 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' |
14 | import { ServerService } from './server' | 14 | import { ServerService } from './server' |
@@ -38,7 +38,6 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification | |||
38 | ], | 38 | ], |
39 | 39 | ||
40 | declarations: [ | 40 | declarations: [ |
41 | ConfirmComponent, | ||
42 | CheatSheetComponent | 41 | CheatSheetComponent |
43 | ], | 42 | ], |
44 | 43 | ||
@@ -48,7 +47,6 @@ import { UserNotificationSocket } from '@app/core/notification/user-notification | |||
48 | 47 | ||
49 | ToastModule, | 48 | ToastModule, |
50 | 49 | ||
51 | ConfirmComponent, | ||
52 | CheatSheetComponent | 50 | CheatSheetComponent |
53 | ], | 51 | ], |
54 | 52 | ||
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 4ae72427b..c868ccdcc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -51,7 +51,10 @@ export class ServerService { | |||
51 | requiresEmailVerification: false | 51 | requiresEmailVerification: false |
52 | }, | 52 | }, |
53 | transcoding: { | 53 | transcoding: { |
54 | enabledResolutions: [] | 54 | enabledResolutions: [], |
55 | hls: { | ||
56 | enabled: false | ||
57 | } | ||
55 | }, | 58 | }, |
56 | avatar: { | 59 | avatar: { |
57 | file: { | 60 | file: { |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index c23e0c55d..46a87c79c 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -5,6 +5,6 @@ | |||
5 | <span (click)="doSearch()" class="icon icon-search"></span> | 5 | <span (click)="doSearch()" class="icon icon-search"></span> |
6 | 6 | ||
7 | <a class="upload-button" routerLink="/videos/upload"> | 7 | <a class="upload-button" routerLink="/videos/upload"> |
8 | <span class="icon icon-upload"></span> | 8 | <my-global-icon iconName="upload"></my-global-icon> |
9 | <span i18n class="upload-button-label">Upload</span> | 9 | <span i18n class="upload-button-label">Upload</span> |
10 | </a> | 10 | </a> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 2f9820665..cea415d9b 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -6,6 +6,7 @@ | |||
6 | padding-left: 10px; | 6 | padding-left: 10px; |
7 | margin-right: 15px; | 7 | margin-right: 15px; |
8 | padding-right: 40px; // For the search icon | 8 | padding-right: 40px; // For the search icon |
9 | font-size: 14px; | ||
9 | 10 | ||
10 | &::placeholder { | 11 | &::placeholder { |
11 | color: var(--inputPlaceholderColor); | 12 | color: var(--inputPlaceholderColor); |
@@ -40,6 +41,7 @@ | |||
40 | .upload-button { | 41 | .upload-button { |
41 | @include peertube-button-link; | 42 | @include peertube-button-link; |
42 | @include orange-button; | 43 | @include orange-button; |
44 | @include button-with-icon(22px, 3px, -1px); | ||
43 | 45 | ||
44 | margin-right: 25px; | 46 | margin-right: 25px; |
45 | 47 | ||
@@ -47,15 +49,6 @@ | |||
47 | margin-right: 0; | 49 | margin-right: 0; |
48 | } | 50 | } |
49 | 51 | ||
50 | .icon.icon-upload { | ||
51 | @include icon(22px); | ||
52 | |||
53 | background-image: url('../../assets/images/header/upload-white.svg'); | ||
54 | height: 24px; | ||
55 | vertical-align: middle; | ||
56 | margin-right: 6px; | ||
57 | } | ||
58 | |||
59 | @media screen and (max-width: 600px) { | 52 | @media screen and (max-width: 600px) { |
60 | margin-right: 10px; | 53 | margin-right: 10px; |
61 | padding: 0 10px; | 54 | padding: 0 10px; |
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 9b8146624..4efe3fb22 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html | |||
@@ -55,7 +55,8 @@ | |||
55 | <ng-template #forgotPasswordModal> | 55 | <ng-template #forgotPasswordModal> |
56 | <div class="modal-header"> | 56 | <div class="modal-header"> |
57 | <h4 i18n class="modal-title">Forgot your password</h4> | 57 | <h4 i18n class="modal-title">Forgot your password</h4> |
58 | <span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span> | 58 | |
59 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon> | ||
59 | </div> | 60 | </div> |
60 | 61 | ||
61 | <div class="modal-body"> | 62 | <div class="modal-body"> |
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss index 807385022..e785db788 100644 --- a/client/src/app/menu/avatar-notification.component.scss +++ b/client/src/app/menu/avatar-notification.component.scss | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | /deep/ { | 4 | /deep/ { |
5 | .popover-notifications.popover { | 5 | .popover-notifications.popover { |
6 | max-width: 400px; | 6 | max-width: none; |
7 | 7 | ||
8 | .popover-body { | 8 | .popover-body { |
9 | padding: 0; | 9 | padding: 0; |
@@ -11,9 +11,8 @@ | |||
11 | font-family: $main-fonts; | 11 | font-family: $main-fonts; |
12 | overflow-y: auto; | 12 | overflow-y: auto; |
13 | max-height: 500px; | 13 | max-height: 500px; |
14 | min-width: 200px; | 14 | width: 400px; |
15 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); | 15 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); |
16 | overflow-y: auto; | ||
17 | 16 | ||
18 | .notifications-header { | 17 | .notifications-header { |
19 | display: flex; | 18 | display: flex; |
@@ -42,7 +41,7 @@ | |||
42 | justify-content: center; | 41 | justify-content: center; |
43 | font-weight: $font-semibold; | 42 | font-weight: $font-semibold; |
44 | color: var(--mainForegroundColor); | 43 | color: var(--mainForegroundColor); |
45 | height: 30px; | 44 | padding: 7px 0; |
46 | } | 45 | } |
47 | } | 46 | } |
48 | } | 47 | } |
@@ -73,7 +72,7 @@ | |||
73 | justify-content: center; | 72 | justify-content: center; |
74 | 73 | ||
75 | background-color: var(--mainColor); | 74 | background-color: var(--mainColor); |
76 | color: var(--mainBackgroundColor); | 75 | color: var(#fff); |
77 | font-size: 10px; | 76 | font-size: 10px; |
78 | font-weight: $font-semibold; | 77 | font-weight: $font-semibold; |
79 | 78 | ||
@@ -82,3 +81,11 @@ | |||
82 | height: 15px; | 81 | height: 15px; |
83 | } | 82 | } |
84 | } | 83 | } |
84 | |||
85 | @media screen and (max-width: $mobile-view) { | ||
86 | /deep/ { | ||
87 | .popover-notifications.popover .popover-body { | ||
88 | width: 400px; | ||
89 | } | ||
90 | } | ||
91 | } | ||
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html index c79609898..a62b33dda 100644 --- a/client/src/app/menu/language-chooser.component.html +++ b/client/src/app/menu/language-chooser.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change the language</h4> | 3 | <h4 i18n class="modal-title">Change the language</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | 7 | ||
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index a4aaadc7f..f30b89413 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -16,7 +16,7 @@ menu { | |||
16 | height: 100%; | 16 | height: 100%; |
17 | white-space: nowrap; | 17 | white-space: nowrap; |
18 | text-overflow: ellipsis; | 18 | text-overflow: ellipsis; |
19 | overflow: hidden; | 19 | overflow: auto; |
20 | color: var(--menuForegroundColor); | 20 | color: var(--menuForegroundColor); |
21 | display: flex; | 21 | display: flex; |
22 | flex-direction: column; | 22 | flex-direction: column; |
@@ -243,7 +243,7 @@ menu { | |||
243 | } | 243 | } |
244 | } | 244 | } |
245 | 245 | ||
246 | @media screen and (max-width: 400px) { | 246 | @media screen and (max-width: $mobile-view) { |
247 | .menu-wrapper { | 247 | .menu-wrapper { |
248 | width: 100% !important; | 248 | width: 100% !important; |
249 | } | 249 | } |
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index 3e074621b..6de13d276 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -87,10 +87,10 @@ | |||
87 | text-overflow: ellipsis; | 87 | text-overflow: ellipsis; |
88 | white-space: nowrap; | 88 | white-space: nowrap; |
89 | font-size: 14px; | 89 | font-size: 14px; |
90 | color: #585858; | 90 | color: $grey-foreground-color; |
91 | 91 | ||
92 | &:hover { | 92 | &:hover { |
93 | color: #303030; | 93 | color: $grey-foreground-hover-color; |
94 | } | 94 | } |
95 | } | 95 | } |
96 | } | 96 | } |
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index 811afb449..adecec1fc 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer { | |||
16 | 16 | ||
17 | avatarUrl: string | 17 | avatarUrl: string |
18 | 18 | ||
19 | static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) { | 19 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) { |
20 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 20 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
21 | 21 | ||
22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path | 22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html index 90651f217..114b1d71f 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" | 3 | class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" |
4 | ngbDropdownToggle role="button" | 4 | ngbDropdownToggle role="button" |
5 | > | 5 | > |
6 | <span *ngIf="!label" class="icon icon-action"></span> | 6 | <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon> |
7 | <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> | 7 | <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> |
8 | </div> | 8 | </div> |
9 | 9 | ||
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index a4fcceeee..985b2ca88 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss | |||
@@ -24,14 +24,11 @@ | |||
24 | } | 24 | } |
25 | 25 | ||
26 | &:hover, &:active, &:focus { | 26 | &:hover, &:active, &:focus { |
27 | background-color: $grey-color; | 27 | background-color: $grey-background-color; |
28 | } | 28 | } |
29 | 29 | ||
30 | .icon-action { | 30 | .more-icon { |
31 | @include icon(21px); | 31 | width: 21px; |
32 | |||
33 | background-image: url('../../../assets/images/video/more.svg'); | ||
34 | top: -1px; | ||
35 | } | 32 | } |
36 | 33 | ||
37 | &.small { | 34 | &.small { |
diff --git a/client/src/app/shared/buttons/button.component.html b/client/src/app/shared/buttons/button.component.html index 87a8daccf..b6df67102 100644 --- a/client/src/app/shared/buttons/button.component.html +++ b/client/src/app/shared/buttons/button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> | 1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> |
2 | <span class="icon" [ngClass]="icon"></span> | 2 | <my-global-icon [iconName]="icon"></my-global-icon> |
3 | <span class="button-label">{{ label }}</span> | 3 | <span class="button-label">{{ label }}</span> |
4 | </span> | 4 | </span> |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 168102f09..04199a2a9 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -3,41 +3,18 @@ | |||
3 | 3 | ||
4 | .action-button { | 4 | .action-button { |
5 | @include peertube-button-link; | 5 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | ||
6 | 7 | ||
7 | font-size: 15px; | ||
8 | font-weight: $font-semibold; | 8 | font-weight: $font-semibold; |
9 | color: #585858; | 9 | color: $grey-foreground-color; |
10 | background-color: #E5E5E5; | 10 | background-color: $grey-background-color; |
11 | 11 | ||
12 | &:hover { | 12 | &:hover { |
13 | background-color: #EFEFEF; | 13 | background-color: $grey-background-hover-color; |
14 | } | 14 | } |
15 | 15 | ||
16 | .icon { | 16 | my-global-icon { |
17 | @include icon(21px); | 17 | @include apply-svg-color($grey-foreground-color); |
18 | |||
19 | position: relative; | ||
20 | top: -2px; | ||
21 | |||
22 | &.icon-edit { | ||
23 | background-image: url('../../../assets/images/global/edit-grey.svg'); | ||
24 | } | ||
25 | |||
26 | &.icon-delete-grey { | ||
27 | background-image: url('../../../assets/images/global/delete-grey.svg'); | ||
28 | } | ||
29 | |||
30 | &.icon-im-with-her { | ||
31 | background-image: url('../../../assets/images/global/im-with-her.svg'); | ||
32 | } | ||
33 | |||
34 | &.icon-tick { | ||
35 | background-image: url('../../../assets/images/global/tick.svg'); | ||
36 | } | ||
37 | |||
38 | &.icon-cross { | ||
39 | background-image: url('../../../assets/images/global/cross.svg'); | ||
40 | } | ||
41 | } | 18 | } |
42 | } | 19 | } |
43 | 20 | ||
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index 1a1162f09..a91e9c7eb 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { GlobalIconName } from '@app/shared/icons/global-icon.component' | ||
2 | 3 | ||
3 | @Component({ | 4 | @Component({ |
4 | selector: 'my-button', | 5 | selector: 'my-button', |
@@ -9,7 +10,7 @@ import { Component, Input } from '@angular/core' | |||
9 | export class ButtonComponent { | 10 | export class ButtonComponent { |
10 | @Input() label = '' | 11 | @Input() label = '' |
11 | @Input() className: string = undefined | 12 | @Input() className: string = undefined |
12 | @Input() icon: string = undefined | 13 | @Input() icon: GlobalIconName = undefined |
13 | @Input() title: string = undefined | 14 | @Input() title: string = undefined |
14 | 15 | ||
15 | getTitle () { | 16 | getTitle () { |
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html index 6c55d8104..4d12a84c0 100644 --- a/client/src/app/shared/buttons/delete-button.component.html +++ b/client/src/app/shared/buttons/delete-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> | 1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> |
2 | <span class="icon icon-delete-grey"></span> | 2 | <my-global-icon iconName="delete"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span class="button-label" i18n *ngIf="!label">Delete</span> | 5 | <span class="button-label" i18n *ngIf="!label">Delete</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html index cecb780f3..da3addbae 100644 --- a/client/src/app/shared/buttons/edit-button.component.html +++ b/client/src/app/shared/buttons/edit-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> | 1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> |
2 | <span class="icon icon-edit"></span> | 2 | <my-global-icon iconName="edit"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span i18n class="button-label" *ngIf="!label">Edit</span> | 5 | <span i18n class="button-label" *ngIf="!label">Edit</span> |
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/shared/confirm/confirm.component.html index 43f0c6190..65df1cd4d 100644 --- a/client/src/app/core/confirm/confirm.component.html +++ b/client/src/app/shared/confirm/confirm.component.html | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | <div class="modal-header"> | 3 | <div class="modal-header"> |
4 | <h4 class="modal-title">{{ title }}</h4> | 4 | <h4 class="modal-title">{{ title }}</h4> |
5 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 5 | |
6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
6 | </div> | 7 | </div> |
7 | 8 | ||
8 | <div class="modal-body" > | 9 | <div class="modal-body" > |
diff --git a/client/src/app/core/confirm/confirm.component.scss b/client/src/app/shared/confirm/confirm.component.scss index 93dd7926b..93dd7926b 100644 --- a/client/src/app/core/confirm/confirm.component.scss +++ b/client/src/app/shared/confirm/confirm.component.scss | |||
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/shared/confirm/confirm.component.ts index 5138b7848..63c163da6 100644 --- a/client/src/app/core/confirm/confirm.component.ts +++ b/client/src/app/shared/confirm/confirm.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { ConfirmService } from './confirm.service' | 2 | import { ConfirmService } from '@app/core/confirm/confirm.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
diff --git a/client/src/app/shared/icons/global-icon.component.html b/client/src/app/shared/icons/global-icon.component.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.html | |||
diff --git a/client/src/app/shared/icons/global-icon.component.scss b/client/src/app/shared/icons/global-icon.component.scss new file mode 100644 index 000000000..6805fb6f7 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.scss | |||
@@ -0,0 +1,4 @@ | |||
1 | /deep/ svg { | ||
2 | width: inherit; | ||
3 | height: inherit; | ||
4 | } | ||
diff --git a/client/src/app/shared/icons/global-icon.component.ts b/client/src/app/shared/icons/global-icon.component.ts new file mode 100644 index 000000000..e8ada0324 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | ||
2 | |||
3 | const icons = { | ||
4 | 'add': require('../../../assets/images/global/add.html'), | ||
5 | 'syndication': require('../../../assets/images/global/syndication.html'), | ||
6 | 'help': require('../../../assets/images/global/help.html'), | ||
7 | 'sparkle': require('../../../assets/images/global/sparkle.html'), | ||
8 | 'alert': require('../../../assets/images/global/alert.html'), | ||
9 | 'cloud-error': require('../../../assets/images/global/cloud-error.html'), | ||
10 | 'user-add': require('../../../assets/images/global/user-add.html'), | ||
11 | 'no': require('../../../assets/images/global/no.html'), | ||
12 | 'cloud-download': require('../../../assets/images/global/cloud-download.html'), | ||
13 | 'undo': require('../../../assets/images/global/undo.html'), | ||
14 | 'circle-tick': require('../../../assets/images/global/circle-tick.html'), | ||
15 | 'cog': require('../../../assets/images/global/cog.html'), | ||
16 | 'download': require('../../../assets/images/global/download.html'), | ||
17 | 'edit': require('../../../assets/images/global/edit.html'), | ||
18 | 'im-with-her': require('../../../assets/images/global/im-with-her.html'), | ||
19 | 'delete': require('../../../assets/images/global/delete.html'), | ||
20 | 'cross': require('../../../assets/images/global/cross.html'), | ||
21 | 'validate': require('../../../assets/images/global/validate.html'), | ||
22 | 'tick': require('../../../assets/images/global/tick.html'), | ||
23 | 'dislike': require('../../../assets/images/video/dislike.html'), | ||
24 | 'heart': require('../../../assets/images/video/heart.html'), | ||
25 | 'like': require('../../../assets/images/video/like.html'), | ||
26 | 'more': require('../../../assets/images/video/more.html'), | ||
27 | 'share': require('../../../assets/images/video/share.html'), | ||
28 | 'upload': require('../../../assets/images/video/upload.html') | ||
29 | } | ||
30 | |||
31 | export type GlobalIconName = keyof typeof icons | ||
32 | |||
33 | @Component({ | ||
34 | selector: 'my-global-icon', | ||
35 | template: '', | ||
36 | styleUrls: [ './global-icon.component.scss' ] | ||
37 | }) | ||
38 | export class GlobalIconComponent implements OnInit { | ||
39 | @Input() iconName: GlobalIconName | ||
40 | |||
41 | constructor (private el: ElementRef) {} | ||
42 | |||
43 | ngOnInit () { | ||
44 | const nativeElement = this.el.nativeElement | ||
45 | |||
46 | nativeElement.innerHTML = icons[this.iconName] | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index 08a2fc367..444425c9f 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html | |||
@@ -25,4 +25,6 @@ | |||
25 | [autoClose]="true" | 25 | [autoClose]="true" |
26 | (onHidden)="onPopoverHidden()" | 26 | (onHidden)="onPopoverHidden()" |
27 | (onShown)="onPopoverShown()" | 27 | (onShown)="onPopoverShown()" |
28 | ></span> | 28 | > |
29 | <my-global-icon iconName="help"></my-global-icon> | ||
30 | </span> | ||
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index 047e53fab..3898f3cda 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss | |||
@@ -2,13 +2,17 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .help-tooltip-button { | 4 | .help-tooltip-button { |
5 | @include icon(17px); | 5 | cursor: pointer; |
6 | |||
7 | position: relative; | ||
8 | top: -2px; | ||
9 | background-image: url('../../../assets/images/global/help.svg'); | ||
10 | border: none; | 6 | border: none; |
11 | margin: 5px; | 7 | |
8 | my-global-icon { | ||
9 | width: 17px; | ||
10 | position: relative; | ||
11 | top: -2px; | ||
12 | margin: 5px; | ||
13 | |||
14 | @include apply-svg-color(var(--mainForegroundColor)) | ||
15 | } | ||
12 | } | 16 | } |
13 | 17 | ||
14 | /deep/ { | 18 | /deep/ { |
@@ -16,16 +20,21 @@ | |||
16 | max-width: 300px; | 20 | max-width: 300px; |
17 | 21 | ||
18 | .popover-body { | 22 | .popover-body { |
23 | font-family: $main-fonts; | ||
19 | text-align: left; | 24 | text-align: left; |
20 | padding: 10px; | 25 | padding: 10px; |
21 | font-size: 13px; | 26 | font-size: 13px; |
22 | font-family: $main-fonts; | 27 | background-color: var(--mainBackgroundColor); |
23 | background-color: #fff; | 28 | color: var(--mainForegroundColor); |
24 | color: #000; | ||
25 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | 29 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); |
26 | 30 | ||
31 | p { | ||
32 | margin-bottom: 0; | ||
33 | } | ||
34 | |||
27 | ul { | 35 | ul { |
28 | padding-left: 20px; | 36 | padding-left: 20px; |
37 | margin-bottom: 0; | ||
29 | } | 38 | } |
30 | } | 39 | } |
31 | } | 40 | } |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.html b/client/src/app/shared/moderation/user-ban-modal.component.html index fa5cb7404..f38ea543d 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.html +++ b/client/src/app/shared/moderation/user-ban-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Ban</h4> | 3 | <h4 i18n class="modal-title">Ban</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -19,7 +20,7 @@ | |||
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Ban this user" class="action-button-submit" | 26 | type="submit" i18n-value value="Ban this user" class="action-button-submit" |
@@ -29,4 +30,4 @@ | |||
29 | </form> | 30 | </form> |
30 | </div> | 31 | </div> |
31 | 32 | ||
32 | </ng-template> \ No newline at end of file | 33 | </ng-template> |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index f755ba0e8..942765301 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -42,7 +42,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
42 | this.openedModal = this.modalService.open(this.modal) | 42 | this.openedModal = this.modalService.open(this.modal) |
43 | } | 43 | } |
44 | 44 | ||
45 | hideBanUserModal () { | 45 | hide () { |
46 | this.usersToBan = undefined | 46 | this.usersToBan = undefined |
47 | this.openedModal.close() | 47 | this.openedModal.close() |
48 | } | 48 | } |
@@ -60,7 +60,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
60 | this.notifier.success(message) | 60 | this.notifier.success(message) |
61 | 61 | ||
62 | this.userBanned.emit(this.usersToBan) | 62 | this.userBanned.emit(this.usersToBan) |
63 | this.hideBanUserModal() | 63 | this.hide() |
64 | }, | 64 | }, |
65 | 65 | ||
66 | err => this.notifier.error(err.message) | 66 | err => this.notifier.error(err.message) |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 384f5d722..6f8625c7e 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -67,6 +67,8 @@ import { UserNotificationService } from '@app/shared/users/user-notification.ser | |||
67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' | 67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' |
68 | import { InstanceService } from '@app/shared/instance/instance.service' | 68 | import { InstanceService } from '@app/shared/instance/instance.service' |
69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' | 69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' |
70 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | ||
71 | import { GlobalIconComponent } from '@app/shared/icons/global-icon.component' | ||
70 | 72 | ||
71 | @NgModule({ | 73 | @NgModule({ |
72 | imports: [ | 74 | imports: [ |
@@ -110,7 +112,9 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
110 | UserBanModalComponent, | 112 | UserBanModalComponent, |
111 | UserModerationDropdownComponent, | 113 | UserModerationDropdownComponent, |
112 | TopMenuDropdownComponent, | 114 | TopMenuDropdownComponent, |
113 | UserNotificationsComponent | 115 | UserNotificationsComponent, |
116 | ConfirmComponent, | ||
117 | GlobalIconComponent | ||
114 | ], | 118 | ], |
115 | 119 | ||
116 | exports: [ | 120 | exports: [ |
@@ -151,6 +155,8 @@ import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/sha | |||
151 | UserModerationDropdownComponent, | 155 | UserModerationDropdownComponent, |
152 | TopMenuDropdownComponent, | 156 | TopMenuDropdownComponent, |
153 | UserNotificationsComponent, | 157 | UserNotificationsComponent, |
158 | ConfirmComponent, | ||
159 | GlobalIconComponent, | ||
154 | 160 | ||
155 | NumberFormatterPipe, | 161 | NumberFormatterPipe, |
156 | ObjectLengthPipe, | 162 | ObjectLengthPipe, |
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts index 5ff816fb8..125d2120c 100644 --- a/client/src/app/shared/users/user-notification.model.ts +++ b/client/src/app/shared/users/user-notification.model.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' | 1 | import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared' |
2 | import { Actor } from '@app/shared/actor/actor.model' | ||
2 | 3 | ||
3 | export class UserNotification implements UserNotificationServer { | 4 | export class UserNotification implements UserNotificationServer { |
4 | id: number | 5 | id: number |
@@ -6,10 +7,7 @@ export class UserNotification implements UserNotificationServer { | |||
6 | read: boolean | 7 | read: boolean |
7 | 8 | ||
8 | video?: VideoInfo & { | 9 | video?: VideoInfo & { |
9 | channel: { | 10 | channel: ActorInfo & { avatarUrl?: string } |
10 | id: number | ||
11 | displayName: string | ||
12 | } | ||
13 | } | 11 | } |
14 | 12 | ||
15 | videoImport?: { | 13 | videoImport?: { |
@@ -23,10 +21,7 @@ export class UserNotification implements UserNotificationServer { | |||
23 | comment?: { | 21 | comment?: { |
24 | id: number | 22 | id: number |
25 | threadId: number | 23 | threadId: number |
26 | account: { | 24 | account: ActorInfo & { avatarUrl?: string } |
27 | id: number | ||
28 | displayName: string | ||
29 | } | ||
30 | video: VideoInfo | 25 | video: VideoInfo |
31 | } | 26 | } |
32 | 27 | ||
@@ -40,18 +35,11 @@ export class UserNotification implements UserNotificationServer { | |||
40 | video: VideoInfo | 35 | video: VideoInfo |
41 | } | 36 | } |
42 | 37 | ||
43 | account?: { | 38 | account?: ActorInfo & { avatarUrl?: string } |
44 | id: number | ||
45 | displayName: string | ||
46 | name: string | ||
47 | } | ||
48 | 39 | ||
49 | actorFollow?: { | 40 | actorFollow?: { |
50 | id: number | 41 | id: number |
51 | follower: { | 42 | follower: ActorInfo & { avatarUrl?: string } |
52 | name: string | ||
53 | displayName: string | ||
54 | } | ||
55 | following: { | 43 | following: { |
56 | type: 'account' | 'channel' | 44 | type: 'account' | 'channel' |
57 | name: string | 45 | name: string |
@@ -76,12 +64,22 @@ export class UserNotification implements UserNotificationServer { | |||
76 | this.read = hash.read | 64 | this.read = hash.read |
77 | 65 | ||
78 | this.video = hash.video | 66 | this.video = hash.video |
67 | if (this.video) this.setAvatarUrl(this.video.channel) | ||
68 | |||
79 | this.videoImport = hash.videoImport | 69 | this.videoImport = hash.videoImport |
70 | |||
80 | this.comment = hash.comment | 71 | this.comment = hash.comment |
72 | if (this.comment) this.setAvatarUrl(this.comment.account) | ||
73 | |||
81 | this.videoAbuse = hash.videoAbuse | 74 | this.videoAbuse = hash.videoAbuse |
75 | |||
82 | this.videoBlacklist = hash.videoBlacklist | 76 | this.videoBlacklist = hash.videoBlacklist |
77 | |||
83 | this.account = hash.account | 78 | this.account = hash.account |
79 | if (this.account) this.setAvatarUrl(this.account) | ||
80 | |||
84 | this.actorFollow = hash.actorFollow | 81 | this.actorFollow = hash.actorFollow |
82 | if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower) | ||
85 | 83 | ||
86 | this.createdAt = hash.createdAt | 84 | this.createdAt = hash.createdAt |
87 | this.updatedAt = hash.updatedAt | 85 | this.updatedAt = hash.updatedAt |
@@ -97,6 +95,7 @@ export class UserNotification implements UserNotificationServer { | |||
97 | 95 | ||
98 | case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: | 96 | case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: |
99 | case UserNotificationType.COMMENT_MENTION: | 97 | case UserNotificationType.COMMENT_MENTION: |
98 | this.accountUrl = this.buildAccountUrl(this.comment.account) | ||
100 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] | 99 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] |
101 | break | 100 | break |
102 | 101 | ||
@@ -138,8 +137,8 @@ export class UserNotification implements UserNotificationServer { | |||
138 | return '/videos/watch/' + video.uuid | 137 | return '/videos/watch/' + video.uuid |
139 | } | 138 | } |
140 | 139 | ||
141 | private buildAccountUrl (account: { name: string }) { | 140 | private buildAccountUrl (account: { name: string, host: string }) { |
142 | return '/accounts/' + account.name | 141 | return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) |
143 | } | 142 | } |
144 | 143 | ||
145 | private buildVideoImportUrl () { | 144 | private buildVideoImportUrl () { |
@@ -150,4 +149,7 @@ export class UserNotification implements UserNotificationServer { | |||
150 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName | 149 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName |
151 | } | 150 | } |
152 | 151 | ||
152 | private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) { | ||
153 | actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) | ||
154 | } | ||
153 | } | 155 | } |
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts index 67ed8f74e..f8a30955d 100644 --- a/client/src/app/shared/users/user-notification.service.ts +++ b/client/src/app/shared/users/user-notification.service.ts | |||
@@ -15,8 +15,6 @@ export class UserNotificationService { | |||
15 | static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' | 15 | static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' |
16 | static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' | 16 | static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' |
17 | 17 | ||
18 | private socket: SocketIOClient.Socket | ||
19 | |||
20 | constructor ( | 18 | constructor ( |
21 | private auth: AuthService, | 19 | private auth: AuthService, |
22 | private authHttp: HttpClient, | 20 | private authHttp: HttpClient, |
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html index 86379d941..0d69e0feb 100644 --- a/client/src/app/shared/users/user-notifications.component.html +++ b/client/src/app/shared/users/user-notifications.component.html | |||
@@ -1,61 +1,101 @@ | |||
1 | <div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> | 1 | <div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> |
2 | 2 | ||
3 | <div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> | 3 | <div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> |
4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }"> | 4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> |
5 | 5 | ||
6 | <div [ngSwitch]="notification.type"> | 6 | <ng-container [ngSwitch]="notification.type"> |
7 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> | 7 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> |
8 | {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> | 8 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" /> |
9 | |||
10 | <div class="message"> | ||
11 | {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> | ||
12 | </div> | ||
9 | </ng-container> | 13 | </ng-container> |
10 | 14 | ||
11 | <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> | 15 | <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> |
12 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted | 16 | <my-global-icon iconName="undo"></my-global-icon> |
17 | |||
18 | <div class="message"> | ||
19 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted | ||
20 | </div> | ||
13 | </ng-container> | 21 | </ng-container> |
14 | 22 | ||
15 | <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> | 23 | <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> |
16 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted | 24 | <my-global-icon iconName="no"></my-global-icon> |
25 | |||
26 | <div class="message"> | ||
27 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted | ||
28 | </div> | ||
17 | </ng-container> | 29 | </ng-container> |
18 | 30 | ||
19 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> | 31 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> |
20 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | 32 | <my-global-icon iconName="alert"></my-global-icon> |
33 | |||
34 | <div class="message"> | ||
35 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | ||
36 | </div> | ||
21 | </ng-container> | 37 | </ng-container> |
22 | 38 | ||
23 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> | 39 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> |
24 | {{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | 40 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
41 | |||
42 | <div class="message"> | ||
43 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | ||
44 | </div> | ||
25 | </ng-container> | 45 | </ng-container> |
26 | 46 | ||
27 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> | 47 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> |
28 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published | 48 | <my-global-icon iconName="sparkle"></my-global-icon> |
49 | |||
50 | <div class="message"> | ||
51 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published | ||
52 | </div> | ||
29 | </ng-container> | 53 | </ng-container> |
30 | 54 | ||
31 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> | 55 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> |
32 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded | 56 | <my-global-icon iconName="cloud-download"></my-global-icon> |
57 | |||
58 | <div class="message"> | ||
59 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded | ||
60 | </div> | ||
33 | </ng-container> | 61 | </ng-container> |
34 | 62 | ||
35 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> | 63 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> |
36 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed | 64 | <my-global-icon iconName="cloud-error"></my-global-icon> |
65 | |||
66 | <div class="message"> | ||
67 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed | ||
68 | </div> | ||
37 | </ng-container> | 69 | </ng-container> |
38 | 70 | ||
39 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> | 71 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> |
40 | User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance | 72 | <my-global-icon iconName="user-add"></my-global-icon> |
73 | |||
74 | <div class="message"> | ||
75 | User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance | ||
76 | </div> | ||
41 | </ng-container> | 77 | </ng-container> |
42 | 78 | ||
43 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> | 79 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> |
44 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following | 80 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> |
45 | 81 | ||
46 | <ng-container *ngIf="notification.actorFollow.following.type === 'channel'"> | 82 | <div class="message"> |
47 | your channel {{ notification.actorFollow.following.displayName }} | 83 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following |
48 | </ng-container> | 84 | |
49 | <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> | 85 | <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container> |
86 | <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> | ||
87 | </div> | ||
50 | </ng-container> | 88 | </ng-container> |
51 | 89 | ||
52 | <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> | 90 | <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> |
53 | {{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> | 91 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> |
92 | |||
93 | <div class="message"> | ||
94 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> | ||
95 | </div> | ||
54 | </ng-container> | 96 | </ng-container> |
55 | </div> | 97 | </ng-container> |
56 | 98 | ||
57 | <div i18n title="Mark as read" class="mark-as-read"> | 99 | <div class="from-date">{{ notification.createdAt | myFromNow }}</div> |
58 | <div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div> | ||
59 | </div> | ||
60 | </div> | 100 | </div> |
61 | </div> | 101 | </div> |
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss index 0ae26ea39..315d504c9 100644 --- a/client/src/app/shared/users/user-notifications.component.scss +++ b/client/src/app/shared/users/user-notifications.component.scss | |||
@@ -1,3 +1,6 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
1 | .no-notification { | 4 | .no-notification { |
2 | display: flex; | 5 | display: flex; |
3 | justify-content: center; | 6 | justify-content: center; |
@@ -7,31 +10,42 @@ | |||
7 | 10 | ||
8 | .notification { | 11 | .notification { |
9 | display: flex; | 12 | display: flex; |
10 | justify-content: space-between; | ||
11 | align-items: center; | 13 | align-items: center; |
12 | font-size: inherit; | 14 | font-size: inherit; |
13 | padding: 15px 10px; | 15 | padding: 15px 5px 15px 10px; |
14 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); | 16 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); |
15 | 17 | ||
16 | .mark-as-read { | 18 | &.unread { |
17 | min-width: 35px; | 19 | background-color: rgba(0, 0, 0, 0.05); |
20 | } | ||
21 | |||
22 | my-global-icon { | ||
23 | width: 24px; | ||
24 | margin-right: 11px; | ||
25 | margin-left: 3px; | ||
18 | 26 | ||
19 | .glyphicon { | 27 | @include apply-svg-color(#333); |
20 | display: none; | ||
21 | cursor: pointer; | ||
22 | color: rgba(20, 20, 20, 0.5) | ||
23 | } | ||
24 | } | 28 | } |
25 | 29 | ||
26 | &.unread { | 30 | .avatar { |
27 | background-color: rgba(0, 0, 0, 0.05); | 31 | @include avatar(30px); |
32 | |||
33 | margin-right: 10px; | ||
34 | } | ||
28 | 35 | ||
29 | &:hover .mark-as-read .glyphicon { | 36 | .message { |
30 | display: block; | 37 | flex-grow: 1; |
31 | 38 | ||
32 | &:hover { | 39 | a { |
33 | color: rgba(20, 20, 20, 0.8); | 40 | font-weight: $font-semibold; |
34 | } | ||
35 | } | 41 | } |
36 | } | 42 | } |
43 | |||
44 | .from-date { | ||
45 | font-size: 0.85em; | ||
46 | color: $grey-foreground-color; | ||
47 | padding-left: 5px; | ||
48 | min-width: 70px; | ||
49 | text-align: right; | ||
50 | } | ||
37 | } | 51 | } |
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts index e3913ba56..b5f9fd399 100644 --- a/client/src/app/shared/users/user-notifications.component.ts +++ b/client/src/app/shared/users/user-notifications.component.ts | |||
@@ -20,11 +20,7 @@ export class UserNotificationsComponent implements OnInit { | |||
20 | // So we can access it in the template | 20 | // So we can access it in the template |
21 | UserNotificationType = UserNotificationType | 21 | UserNotificationType = UserNotificationType |
22 | 22 | ||
23 | componentPagination: ComponentPagination = { | 23 | componentPagination: ComponentPagination |
24 | currentPage: 1, | ||
25 | itemsPerPage: this.itemsPerPage, | ||
26 | totalItems: null | ||
27 | } | ||
28 | 24 | ||
29 | constructor ( | 25 | constructor ( |
30 | private userNotificationService: UserNotificationService, | 26 | private userNotificationService: UserNotificationService, |
@@ -32,6 +28,12 @@ export class UserNotificationsComponent implements OnInit { | |||
32 | ) { } | 28 | ) { } |
33 | 29 | ||
34 | ngOnInit () { | 30 | ngOnInit () { |
31 | this.componentPagination = { | ||
32 | currentPage: 1, | ||
33 | itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable | ||
34 | totalItems: null | ||
35 | } | ||
36 | |||
35 | this.loadMoreNotifications() | 37 | this.loadMoreNotifications() |
36 | } | 38 | } |
37 | 39 | ||
@@ -58,6 +60,8 @@ export class UserNotificationsComponent implements OnInit { | |||
58 | } | 60 | } |
59 | 61 | ||
60 | markAsRead (notification: UserNotification) { | 62 | markAsRead (notification: UserNotification) { |
63 | if (notification.read) return | ||
64 | |||
61 | this.userNotificationService.markAsRead(notification) | 65 | this.userNotificationService.markAsRead(notification) |
62 | .subscribe( | 66 | .subscribe( |
63 | () => { | 67 | () => { |
diff --git a/client/src/app/shared/video/feed.component.html b/client/src/app/shared/video/feed.component.html index 16116ba88..f7624ec01 100644 --- a/client/src/app/shared/video/feed.component.html +++ b/client/src/app/shared/video/feed.component.html | |||
@@ -1,10 +1,11 @@ | |||
1 | <div class="video-feed"> | 1 | <div class="video-feed"> |
2 | <span | 2 | <my-global-icon |
3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" | 3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" |
4 | class="icon icon-syndication" role="button" | 4 | class="icon-syndication" role="button" iconName="syndication" |
5 | ></span> | 5 | > |
6 | </my-global-icon> | ||
6 | 7 | ||
7 | <ng-template #feedsList> | 8 | <ng-template #feedsList> |
8 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> | 9 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> |
9 | </ng-template> | 10 | </ng-template> |
10 | </div> \ No newline at end of file | 11 | </div> |
diff --git a/client/src/app/shared/video/feed.component.scss b/client/src/app/shared/video/feed.component.scss index 385764be0..ed1dc17d3 100644 --- a/client/src/app/shared/video/feed.component.scss +++ b/client/src/app/shared/video/feed.component.scss | |||
@@ -1,3 +1,4 @@ | |||
1 | @import '_variables'; | ||
1 | @import '_mixins'; | 2 | @import '_mixins'; |
2 | 3 | ||
3 | .video-feed { | 4 | .video-feed { |
@@ -6,14 +7,12 @@ | |||
6 | display: block; | 7 | display: block; |
7 | } | 8 | } |
8 | 9 | ||
9 | .icon { | 10 | my-global-icon { |
10 | @include icon(12px); | 11 | cursor: pointer; |
12 | width: 12px; | ||
13 | position: relative; | ||
14 | top: -2px; | ||
11 | 15 | ||
12 | &.icon-syndication { | 16 | @include apply-svg-color(var(--mainForegroundColor)) |
13 | position: relative; | ||
14 | top: -2px; | ||
15 | background-color: var(--mainForegroundColor); | ||
16 | mask-image: url('../../../assets/images/global/syndication.svg'); | ||
17 | } | ||
18 | } | 17 | } |
19 | } \ No newline at end of file | 18 | } |
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index fa4ca7f93..f44b4138b 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core' | |||
3 | import { Video } from '../../shared/video/video.model' | 3 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model' | ||
7 | import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type' | ||
6 | 8 | ||
7 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 9 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
8 | descriptionPath: string | 10 | descriptionPath: string |
@@ -19,6 +21,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
19 | likesPercent: number | 21 | likesPercent: number |
20 | dislikesPercent: number | 22 | dislikesPercent: number |
21 | 23 | ||
24 | trackerUrls: string[] | ||
25 | |||
26 | streamingPlaylists: VideoStreamingPlaylist[] | ||
27 | |||
22 | constructor (hash: VideoDetailsServerModel, translations = {}) { | 28 | constructor (hash: VideoDetailsServerModel, translations = {}) { |
23 | super(hash, translations) | 29 | super(hash, translations) |
24 | 30 | ||
@@ -30,6 +36,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
30 | this.support = hash.support | 36 | this.support = hash.support |
31 | this.commentsEnabled = hash.commentsEnabled | 37 | this.commentsEnabled = hash.commentsEnabled |
32 | 38 | ||
39 | this.trackerUrls = hash.trackerUrls | ||
40 | this.streamingPlaylists = hash.streamingPlaylists | ||
41 | |||
33 | this.buildLikeAndDislikePercents() | 42 | this.buildLikeAndDislikePercents() |
34 | } | 43 | } |
35 | 44 | ||
@@ -53,4 +62,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
53 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 | 62 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 |
54 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 | 63 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 |
55 | } | 64 | } |
65 | |||
66 | getHlsPlaylist () { | ||
67 | return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
68 | } | ||
56 | } | 69 | } |
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index 895879adc..f44bdf9a9 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss | |||
@@ -50,10 +50,10 @@ | |||
50 | text-overflow: ellipsis; | 50 | text-overflow: ellipsis; |
51 | white-space: nowrap; | 51 | white-space: nowrap; |
52 | font-size: 13px; | 52 | font-size: 13px; |
53 | color: #585858; | 53 | color: $grey-foreground-color; |
54 | 54 | ||
55 | &:hover { | 55 | &:hover { |
56 | color: #303030; | 56 | color: $grey-foreground-hover-color; |
57 | } | 57 | } |
58 | } | 58 | } |
59 | } | 59 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index b92c96450..6ea83d13b 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -53,7 +53,7 @@ export class Video implements VideoServerModel { | |||
53 | displayName: string | 53 | displayName: string |
54 | url: string | 54 | url: string |
55 | host: string | 55 | host: string |
56 | avatar: Avatar | 56 | avatar?: Avatar |
57 | } | 57 | } |
58 | 58 | ||
59 | channel: { | 59 | channel: { |
@@ -63,7 +63,7 @@ export class Video implements VideoServerModel { | |||
63 | displayName: string | 63 | displayName: string |
64 | url: string | 64 | url: string |
65 | host: string | 65 | host: string |
66 | avatar: Avatar | 66 | avatar?: Avatar |
67 | } | 67 | } |
68 | 68 | ||
69 | userHistory?: { | 69 | userHistory?: { |
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html index 30aefdbfc..19043eee6 100644 --- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | <div class="modal-header"> | 4 | <div class="modal-header"> |
5 | <h4 i18n class="modal-title">Add caption</h4> | 5 | <h4 i18n class="modal-title">Add caption</h4> |
6 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <div class="modal-body"> | 9 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index bd52d686a..092c0e862 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -143,7 +143,7 @@ | |||
143 | 143 | ||
144 | <div class="captions-header"> | 144 | <div class="captions-header"> |
145 | <a (click)="openAddCaptionModal()" class="create-caption"> | 145 | <a (click)="openAddCaptionModal()" class="create-caption"> |
146 | <span class="icon icon-add"></span> | 146 | <my-global-icon iconName="add"></my-global-icon> |
147 | <ng-container i18n>Add another caption</ng-container> | 147 | <ng-container i18n>Add another caption</ng-container> |
148 | </a> | 148 | </a> |
149 | </div> | 149 | </div> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 25db8e8ed..bb775cb0a 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -23,10 +23,6 @@ my-peertube-checkbox { | |||
23 | display: block; | 23 | display: block; |
24 | } | 24 | } |
25 | 25 | ||
26 | input, select { | ||
27 | font-size: 15px | ||
28 | } | ||
29 | |||
30 | .label-tags + span { | 26 | .label-tags + span { |
31 | font-size: 15px; | 27 | font-size: 15px; |
32 | } | 28 | } |
@@ -42,7 +38,7 @@ my-peertube-checkbox { | |||
42 | text-align: right; | 38 | text-align: right; |
43 | 39 | ||
44 | .create-caption { | 40 | .create-caption { |
45 | @include create-button('../../../../assets/images/global/add.svg'); | 41 | @include create-button; |
46 | } | 42 | } |
47 | } | 43 | } |
48 | 44 | ||
@@ -100,13 +96,14 @@ my-peertube-checkbox { | |||
100 | display: inline-block; | 96 | display: inline-block; |
101 | margin-right: 25px; | 97 | margin-right: 25px; |
102 | 98 | ||
103 | color: #585858; | 99 | color: $grey-foreground-color; |
104 | font-size: 15px; | 100 | font-size: 15px; |
105 | } | 101 | } |
106 | 102 | ||
107 | .submit-button { | 103 | .submit-button { |
108 | @include peertube-button; | 104 | @include peertube-button; |
109 | @include orange-button; | 105 | @include orange-button; |
106 | @include button-with-icon(20px, 1px); | ||
110 | 107 | ||
111 | display: inline-block; | 108 | display: inline-block; |
112 | 109 | ||
@@ -119,16 +116,6 @@ my-peertube-checkbox { | |||
119 | color: inherit; | 116 | color: inherit; |
120 | font-weight: $font-semibold; | 117 | font-weight: $font-semibold; |
121 | } | 118 | } |
122 | |||
123 | .icon.icon-validate { | ||
124 | @include icon(20px); | ||
125 | |||
126 | cursor: inherit; | ||
127 | position: relative; | ||
128 | top: -1px; | ||
129 | margin-right: 4px; | ||
130 | background-image: url('../../../../assets/images/global/validate.svg'); | ||
131 | } | ||
132 | } | 119 | } |
133 | } | 120 | } |
134 | 121 | ||
@@ -176,10 +163,10 @@ p-calendar { | |||
176 | } | 163 | } |
177 | 164 | ||
178 | tag { | 165 | tag { |
179 | background-color: var(--inputColor) !important; | 166 | background-color: $grey-background-color !important; |
167 | color: #000 !important; | ||
180 | border-radius: 3px !important; | 168 | border-radius: 3px !important; |
181 | font-size: 15px !important; | 169 | font-size: 15px !important; |
182 | color: var(--mainForegroundColor) !important; | ||
183 | height: 30px !important; | 170 | height: 30px !important; |
184 | line-height: 30px !important; | 171 | line-height: 30px !important; |
185 | margin: 0 5px 0 0 !important; | 172 | margin: 0 5px 0 0 !important; |
@@ -202,7 +189,10 @@ p-calendar { | |||
202 | top: -1px; | 189 | top: -1px; |
203 | height: auto !important; | 190 | height: auto !important; |
204 | vertical-align: middle !important; | 191 | vertical-align: middle !important; |
205 | fill: #585858 !important; | 192 | |
193 | path { | ||
194 | fill: $grey-foreground-color !important; | ||
195 | } | ||
206 | } | 196 | } |
207 | 197 | ||
208 | &:hover { | 198 | &:hover { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html index 11a81ad66..28eb143c9 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-torrent"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the torrent to import</span> | 6 | <span i18n>Select the torrent to import</span> |
@@ -66,7 +66,7 @@ | |||
66 | (click)="updateSecondStep()" | 66 | (click)="updateSecondStep()" |
67 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 67 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
68 | > | 68 | > |
69 | <span class="icon icon-validate"></span> | 69 | <my-global-icon iconName="validate"></my-global-icon> |
70 | <input type="button" i18n-value value="Update" /> | 70 | <input type="button" i18n-value value="Update" /> |
71 | </div> | 71 | </div> |
72 | </div> | 72 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss index 00626cd7b..6d59ed834 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss | |||
@@ -1,45 +1,7 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | $width-size: 190px; | 4 | .first-step-block { |
5 | |||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .alert.alert-danger { | ||
11 | text-align: center; | ||
12 | |||
13 | & > div { | ||
14 | font-weight: $font-semibold; | ||
15 | } | ||
16 | } | ||
17 | |||
18 | .import-video-torrent { | ||
19 | display: flex; | ||
20 | flex-direction: column; | ||
21 | align-items: center; | ||
22 | |||
23 | .icon.icon-upload { | ||
24 | @include icon(90px); | ||
25 | margin-bottom: 25px; | ||
26 | cursor: default; | ||
27 | |||
28 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
29 | } | ||
30 | |||
31 | .button-file { | ||
32 | @include peertube-button-file(auto); | ||
33 | |||
34 | min-width: 190px; | ||
35 | } | ||
36 | |||
37 | .button-file-extension { | ||
38 | display: block; | ||
39 | font-size: 12px; | ||
40 | margin-top: 5px; | ||
41 | } | ||
42 | |||
43 | .torrent-or-magnet { | 5 | .torrent-or-magnet { |
44 | margin: 10px 0; | 6 | margin: 10px 0; |
45 | } | 7 | } |
@@ -47,19 +9,6 @@ $width-size: 190px; | |||
47 | .form-group-magnet-uri { | 9 | .form-group-magnet-uri { |
48 | margin-bottom: 40px; | 10 | margin-bottom: 40px; |
49 | } | 11 | } |
50 | |||
51 | input[type=text] { | ||
52 | @include peertube-input-text($width-size); | ||
53 | display: block; | ||
54 | } | ||
55 | |||
56 | input[type=button] { | ||
57 | @include peertube-button; | ||
58 | @include orange-button; | ||
59 | |||
60 | width: $width-size; | ||
61 | margin-top: 30px; | ||
62 | } | ||
63 | } | 12 | } |
64 | 13 | ||
65 | 14 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts index 63db06919..307806bb9 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts | |||
@@ -18,7 +18,8 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
18 | templateUrl: './video-import-torrent.component.html', | 18 | templateUrl: './video-import-torrent.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-torrent.component.scss' | 21 | './video-import-torrent.component.scss', |
22 | './video-send.scss' | ||
22 | ] | 23 | ] |
23 | }) | 24 | }) |
24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html index 533446672..3550c3585 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-url"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="form-group"> | 5 | <div class="form-group"> |
6 | <label i18n for="targetUrl">URL</label> | 6 | <label i18n for="targetUrl">URL</label> |
@@ -59,7 +59,7 @@ | |||
59 | (click)="updateSecondStep()" | 59 | (click)="updateSecondStep()" |
60 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 60 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
61 | > | 61 | > |
62 | <span class="icon icon-validate"></span> | 62 | <my-global-icon iconName="validate"></my-global-icon> |
63 | <input type="button" i18n-value value="Update" /> | 63 | <input type="button" i18n-value value="Update" /> |
64 | </div> | 64 | </div> |
65 | </div> | 65 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts index a1810b7a0..257c6e5db 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts | |||
@@ -18,7 +18,7 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
18 | templateUrl: './video-import-url.component.html', | 18 | templateUrl: './video-import-url.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-url.component.scss' | 21 | './video-send.scss' |
22 | ] | 22 | ] |
23 | }) | 23 | }) |
24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss index e907edc70..8769dd302 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss | |||
@@ -3,10 +3,6 @@ | |||
3 | 3 | ||
4 | $width-size: 190px; | 4 | $width-size: 190px; |
5 | 5 | ||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .alert.alert-danger { | 6 | .alert.alert-danger { |
11 | text-align: center; | 7 | text-align: center; |
12 | 8 | ||
@@ -15,17 +11,20 @@ $width-size: 190px; | |||
15 | } | 11 | } |
16 | } | 12 | } |
17 | 13 | ||
18 | .import-video-url { | 14 | .first-step-block { |
19 | display: flex; | 15 | display: flex; |
20 | flex-direction: column; | 16 | flex-direction: column; |
21 | align-items: center; | 17 | align-items: center; |
22 | 18 | ||
23 | .icon.icon-upload { | 19 | .upload-icon { |
24 | @include icon(90px); | 20 | width: 90px; |
25 | margin-bottom: 25px; | 21 | margin-bottom: 25px; |
26 | cursor: default; | ||
27 | 22 | ||
28 | background-image: url('../../../../assets/images/video/upload.svg'); | 23 | @include apply-svg-color(#C6C6C6); |
24 | } | ||
25 | |||
26 | .peertube-select-container { | ||
27 | @include peertube-select-container($width-size); | ||
29 | } | 28 | } |
30 | 29 | ||
31 | input[type=text] { | 30 | input[type=text] { |
@@ -40,6 +39,16 @@ $width-size: 190px; | |||
40 | width: $width-size; | 39 | width: $width-size; |
41 | margin-top: 30px; | 40 | margin-top: 30px; |
42 | } | 41 | } |
43 | } | ||
44 | 42 | ||
43 | .button-file { | ||
44 | @include peertube-button-file(auto); | ||
45 | 45 | ||
46 | min-width: 190px; | ||
47 | } | ||
48 | |||
49 | .button-file-extension { | ||
50 | display: block; | ||
51 | font-size: 12px; | ||
52 | margin-top: 5px; | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html index 826e54d25..b252cd60a 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> | 1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> |
2 | <div class="upload-video"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the file to upload</span> | 6 | <span i18n>Select the file to upload</span> |
@@ -61,7 +61,7 @@ | |||
61 | (click)="updateSecondStep()" | 61 | (click)="updateSecondStep()" |
62 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" | 62 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" |
63 | > | 63 | > |
64 | <span class="icon icon-validate"></span> | 64 | <my-global-icon iconName="validate"></my-global-icon> |
65 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> | 65 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> |
66 | </div> | 66 | </div> |
67 | </div> | 67 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss index 4b2c86ae9..8adf8f169 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -1,47 +1,9 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .peertube-select-container { | 4 | .first-step-block .form-group-channel { |
5 | @include peertube-select-container(190px); | 5 | margin-bottom: 20px; |
6 | } | 6 | margin-top: 35px; |
7 | |||
8 | .alert.alert-danger { | ||
9 | text-align: center; | ||
10 | |||
11 | & > div { | ||
12 | font-weight: $font-semibold; | ||
13 | } | ||
14 | } | ||
15 | |||
16 | .upload-video { | ||
17 | display: flex; | ||
18 | flex-direction: column; | ||
19 | align-items: center; | ||
20 | |||
21 | .form-group-channel { | ||
22 | margin-bottom: 20px; | ||
23 | margin-top: 35px; | ||
24 | } | ||
25 | |||
26 | .icon.icon-upload { | ||
27 | @include icon(90px); | ||
28 | margin-bottom: 25px; | ||
29 | cursor: default; | ||
30 | |||
31 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
32 | } | ||
33 | |||
34 | .button-file { | ||
35 | @include peertube-button-file(auto); | ||
36 | |||
37 | min-width: 190px; | ||
38 | } | ||
39 | |||
40 | .button-file-extension { | ||
41 | display: block; | ||
42 | font-size: 12px; | ||
43 | margin-top: 5px; | ||
44 | } | ||
45 | } | 7 | } |
46 | 8 | ||
47 | .upload-progress-cancel { | 9 | .upload-progress-cancel { |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index aa40f8781..e4d54b654 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -20,7 +20,8 @@ import { scrollToTop } from '@app/shared/misc/utils' | |||
20 | templateUrl: './video-upload.component.html', | 20 | templateUrl: './video-upload.component.html', |
21 | styleUrls: [ | 21 | styleUrls: [ |
22 | '../shared/video-edit.component.scss', | 22 | '../shared/video-edit.component.scss', |
23 | './video-upload.component.scss' | 23 | './video-upload.component.scss', |
24 | './video-send.scss' | ||
24 | ] | 25 | ] |
25 | }) | 26 | }) |
26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index 0457778c0..4992bb369 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -13,7 +13,7 @@ | |||
13 | 13 | ||
14 | <div class="submit-container"> | 14 | <div class="submit-container"> |
15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> | 15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> |
16 | <span class="icon icon-validate"></span> | 16 | <my-global-icon iconName="validate"></my-global-icon> |
17 | <input type="button" i18n-value value="Update" /> | 17 | <input type="button" i18n-value value="Update" /> |
18 | </div> | 18 | </div> |
19 | </div> | 19 | </div> |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss index 84da5727e..731ecbf8f 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss | |||
@@ -41,7 +41,7 @@ | |||
41 | } | 41 | } |
42 | 42 | ||
43 | .comment-date { | 43 | .comment-date { |
44 | color: #585858; | 44 | color: $grey-foreground-color; |
45 | margin-left: 10px; | 45 | margin-left: 10px; |
46 | } | 46 | } |
47 | } | 47 | } |
@@ -69,7 +69,7 @@ | |||
69 | 69 | ||
70 | .comment-action-reply, | 70 | .comment-action-reply, |
71 | .comment-action-delete { | 71 | .comment-action-delete { |
72 | color: #585858; | 72 | color: $grey-foreground-color; |
73 | cursor: pointer; | 73 | cursor: pointer; |
74 | margin-right: 10px; | 74 | margin-right: 10px; |
75 | 75 | ||
@@ -108,4 +108,4 @@ | |||
108 | .root-comment { | 108 | .root-comment { |
109 | font-size: 14px; | 109 | font-size: 14px; |
110 | } | 110 | } |
111 | } \ No newline at end of file | 111 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html index 83600fa81..1a87bdcd4 100644 --- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Blacklist video</h4> | 3 | <h4 i18n class="modal-title">Blacklist video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html index f46f92a17..2bb5d6d37 100644 --- a/client/src/app/videos/+video-watch/modal/video-download.component.html +++ b/client/src/app/videos/+video-watch/modal/video-download.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Download video</h4> | 3 | <h4 i18n class="modal-title">Download video</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html index 733c01be0..b9434da26 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.html +++ b/client/src/app/videos/+video-watch/modal/video-report.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Report video</h4> | 3 | <h4 i18n class="modal-title">Report video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html index 301f67f2d..9f3c37fe8 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.html +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Share</h4> | 3 | <h4 i18n class="modal-title">Share</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html index 00c304709..2a05224a8 100644 --- a/client/src/app/videos/+video-watch/modal/video-support.component.html +++ b/client/src/app/videos/+video-watch/modal/video-support.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Support</h4> | 3 | <h4 i18n class="modal-title">Support</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> | 7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> |
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 770785d02..709eb91a8 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -53,55 +53,57 @@ | |||
53 | <div | 53 | <div |
54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" | 54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" |
55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" | 55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" |
56 | i18n-title title="Like this video" | ||
56 | > | 57 | > |
57 | <span class="icon icon-like" i18n-title title="Like this video" ></span> | 58 | <my-global-icon iconName="like"></my-global-icon> |
58 | </div> | 59 | </div> |
59 | 60 | ||
60 | <div | 61 | <div |
61 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" | 62 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" |
62 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" | 63 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" |
64 | i18n-title title="Dislike this video" | ||
63 | > | 65 | > |
64 | <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> | 66 | <my-global-icon iconName="dislike"></my-global-icon> |
65 | </div> | 67 | </div> |
66 | 68 | ||
67 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> | 69 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> |
68 | <span class="icon icon-support"></span> | 70 | <my-global-icon iconName="heart"></my-global-icon> |
69 | <span class="icon-text" i18n>Support</span> | 71 | <span class="icon-text" i18n>Support</span> |
70 | </div> | 72 | </div> |
71 | 73 | ||
72 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> | 74 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> |
73 | <span class="icon icon-share"></span> | 75 | <my-global-icon iconName="share"></my-global-icon> |
74 | <span class="icon-text" i18n>Share</span> | 76 | <span class="icon-text" i18n>Share</span> |
75 | </div> | 77 | </div> |
76 | 78 | ||
77 | <div class="action-more" ngbDropdown placement="top" role="button"> | 79 | <div class="action-more" ngbDropdown placement="top" role="button"> |
78 | <div class="action-button" ngbDropdownToggle role="button"> | 80 | <div class="action-button" ngbDropdownToggle role="button"> |
79 | <span class="icon icon-more"></span> | 81 | <my-global-icon class="more-icon" iconName="more"></my-global-icon> |
80 | </div> | 82 | </div> |
81 | 83 | ||
82 | <div ngbDropdownMenu> | 84 | <div ngbDropdownMenu> |
83 | <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> | 85 | <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> |
84 | <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> | 86 | <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container> |
85 | </a> | 87 | </a> |
86 | 88 | ||
87 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> | 89 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> |
88 | <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> | 90 | <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container> |
89 | </a> | 91 | </a> |
90 | 92 | ||
91 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> | 93 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> |
92 | <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> | 94 | <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container> |
93 | </a> | 95 | </a> |
94 | 96 | ||
95 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> | 97 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> |
96 | <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> | 98 | <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container> |
97 | </a> | 99 | </a> |
98 | 100 | ||
99 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> | 101 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> |
100 | <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> | 102 | <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container> |
101 | </a> | 103 | </a> |
102 | 104 | ||
103 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> | 105 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> |
104 | <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> | 106 | <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container> |
105 | </a> | 107 | </a> |
106 | </div> | 108 | </div> |
107 | </div> | 109 | </div> |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index 2586a2204..b03ed197d 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -183,6 +183,8 @@ $other-videos-width: 260px; | |||
183 | .action-button { | 183 | .action-button { |
184 | @include peertube-button; | 184 | @include peertube-button; |
185 | @include grey-button; | 185 | @include grey-button; |
186 | @include button-with-icon(21px, 0, -1px); | ||
187 | @include apply-svg-color($grey-foreground-color); | ||
186 | 188 | ||
187 | font-size: 15px; | 189 | font-size: 15px; |
188 | font-weight: $font-semibold; | 190 | font-weight: $font-semibold; |
@@ -194,53 +196,25 @@ $other-videos-width: 260px; | |||
194 | display: none; | 196 | display: none; |
195 | } | 197 | } |
196 | 198 | ||
197 | .icon { | ||
198 | @include icon(21px); | ||
199 | |||
200 | position: relative; | ||
201 | top: -2px; | ||
202 | |||
203 | &.icon-like { | ||
204 | background-image: url('../../../assets/images/video/like-grey.svg'); | ||
205 | } | ||
206 | |||
207 | &.icon-dislike { | ||
208 | background-image: url('../../../assets/images/video/dislike-grey.svg'); | ||
209 | } | ||
210 | |||
211 | &.icon-support { | ||
212 | background-image: url('../../../assets/images/video/heart.svg'); | ||
213 | } | ||
214 | |||
215 | &.icon-share { | ||
216 | background-image: url('../../../assets/images/video/share.svg'); | ||
217 | } | ||
218 | |||
219 | &.icon-more { | ||
220 | background-image: url('../../../assets/images/video/more.svg'); | ||
221 | top: -1px; | ||
222 | } | ||
223 | } | ||
224 | |||
225 | .icon-text { | ||
226 | margin-left: 3px; | ||
227 | } | ||
228 | |||
229 | &.action-button-like.activated { | 199 | &.action-button-like.activated { |
230 | background-color: $green; | 200 | background-color: $green; |
231 | 201 | ||
232 | .icon-like { | 202 | my-global-icon { |
233 | background-image: url('../../../assets/images/video/like-white.svg'); | 203 | @include apply-svg-color(#fff); |
234 | } | 204 | } |
235 | } | 205 | } |
236 | 206 | ||
237 | &.action-button-dislike.activated { | 207 | &.action-button-dislike.activated { |
238 | background-color: $red; | 208 | background-color: $red; |
239 | 209 | ||
240 | .icon-dislike { | 210 | my-global-icon { |
241 | background-image: url('../../../assets/images/video/dislike-white.svg'); | 211 | @include apply-svg-color(#fff); |
242 | } | 212 | } |
243 | } | 213 | } |
214 | |||
215 | .icon-text { | ||
216 | margin-left: 3px; | ||
217 | } | ||
244 | } | 218 | } |
245 | 219 | ||
246 | .action-more { | 220 | .action-more { |
@@ -249,36 +223,12 @@ $other-videos-width: 260px; | |||
249 | .dropdown-menu .dropdown-item { | 223 | .dropdown-menu .dropdown-item { |
250 | padding: 6px 24px; | 224 | padding: 6px 24px; |
251 | 225 | ||
252 | .icon { | 226 | my-global-icon { |
253 | @include icon(24px); | 227 | width: 24px; |
254 | 228 | ||
255 | margin-right: 10px; | 229 | margin-right: 10px; |
256 | position: relative; | 230 | position: relative; |
257 | top: -1px; | 231 | top: -2px; |
258 | |||
259 | &.icon-download { | ||
260 | background-image: url('../../../assets/images/video/download-black.svg'); | ||
261 | } | ||
262 | |||
263 | &.icon-edit { | ||
264 | background-image: url('../../../assets/images/global/edit-black.svg'); | ||
265 | } | ||
266 | |||
267 | &.icon-alert { | ||
268 | background-image: url('../../../assets/images/video/alert.svg'); | ||
269 | } | ||
270 | |||
271 | &.icon-blacklist { | ||
272 | background-image: url('../../../assets/images/video/blacklist.svg'); | ||
273 | } | ||
274 | |||
275 | &.icon-unblacklist { | ||
276 | background-image: url('../../../assets/images/global/undo.svg'); | ||
277 | } | ||
278 | |||
279 | &.icon-delete { | ||
280 | background-image: url('../../../assets/images/global/delete-black.svg'); | ||
281 | } | ||
282 | } | 232 | } |
283 | } | 233 | } |
284 | } | 234 | } |
@@ -320,7 +270,7 @@ $other-videos-width: 260px; | |||
320 | .video-info-description-more { | 270 | .video-info-description-more { |
321 | cursor: pointer; | 271 | cursor: pointer; |
322 | font-weight: $font-semibold; | 272 | font-weight: $font-semibold; |
323 | color: #585858; | 273 | color: $grey-foreground-color; |
324 | font-size: 14px; | 274 | font-size: 14px; |
325 | 275 | ||
326 | .glyphicon { | 276 | .glyphicon { |
@@ -339,7 +289,7 @@ $other-videos-width: 260px; | |||
339 | min-width: 91px; | 289 | min-width: 91px; |
340 | padding-right: 5px; | 290 | padding-right: 5px; |
341 | display: inline-block; | 291 | display: inline-block; |
342 | color: #585858; | 292 | color: $grey-foreground-color; |
343 | font-weight: $font-bold; | 293 | font-weight: $font-bold; |
344 | } | 294 | } |
345 | 295 | ||
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index ee504bc58..e801f03ad 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -7,14 +7,8 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp | |||
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { Notifier, ServerService } from '@app/core' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Subscription } from 'rxjs' |
10 | // FIXME: something weird with our path definition in tsconfig and typings | ||
11 | // @ts-ignore | ||
12 | import videojs from 'video.js' | ||
13 | import 'videojs-hotkeys' | ||
14 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
15 | import * as WebTorrent from 'webtorrent' | ||
16 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' | 11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' |
17 | import '../../../assets/player/peertube-videojs-plugin' | ||
18 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
19 | import { RestExtractor, VideoBlacklistService } from '../../shared' | 13 | import { RestExtractor, VideoBlacklistService } from '../../shared' |
20 | import { VideoDetails } from '../../shared/video/video-details.model' | 14 | import { VideoDetails } from '../../shared/video/video-details.model' |
@@ -24,12 +18,16 @@ import { VideoReportComponent } from './modal/video-report.component' | |||
24 | import { VideoShareComponent } from './modal/video-share.component' | 18 | import { VideoShareComponent } from './modal/video-share.component' |
25 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' | 19 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' |
26 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' | 20 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' |
27 | import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player' | ||
28 | import { I18n } from '@ngx-translate/i18n-polyfill' | 21 | import { I18n } from '@ngx-translate/i18n-polyfill' |
29 | import { environment } from '../../../environments/environment' | 22 | import { environment } from '../../../environments/environment' |
30 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
31 | import { VideoCaptionService } from '@app/shared/video-caption' | 23 | import { VideoCaptionService } from '@app/shared/video-caption' |
32 | import { MarkdownService } from '@app/shared/renderer' | 24 | import { MarkdownService } from '@app/shared/renderer' |
25 | import { | ||
26 | P2PMediaLoaderOptions, | ||
27 | PeertubePlayerManager, | ||
28 | PeertubePlayerManagerOptions, | ||
29 | PlayerMode | ||
30 | } from '../../../assets/player/peertube-player-manager' | ||
33 | 31 | ||
34 | @Component({ | 32 | @Component({ |
35 | selector: 'my-video-watch', | 33 | selector: 'my-video-watch', |
@@ -46,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
46 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent | 44 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent |
47 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 45 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
48 | 46 | ||
49 | player: videojs.Player | 47 | player: any |
50 | playerElement: HTMLVideoElement | 48 | playerElement: HTMLVideoElement |
51 | userRating: UserVideoRateType = null | 49 | userRating: UserVideoRateType = null |
52 | video: VideoDetails = null | 50 | video: VideoDetails = null |
@@ -61,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
61 | remoteServerDown = false | 59 | remoteServerDown = false |
62 | hotkeys: Hotkey[] | 60 | hotkeys: Hotkey[] |
63 | 61 | ||
64 | private videojsLocaleLoaded = false | ||
65 | private paramsSub: Subscription | 62 | private paramsSub: Subscription |
66 | 63 | ||
67 | constructor ( | 64 | constructor ( |
@@ -92,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
92 | 89 | ||
93 | ngOnInit () { | 90 | ngOnInit () { |
94 | if ( | 91 | if ( |
95 | WebTorrent.WEBRTC_SUPPORT === false || | 92 | !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false || |
96 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' | 93 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' |
97 | ) { | 94 | ) { |
98 | this.hasAlreadyAcceptedPrivacyConcern = true | 95 | this.hasAlreadyAcceptedPrivacyConcern = true |
@@ -118,8 +115,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
118 | .subscribe(([ video, captionsResult ]) => { | 115 | .subscribe(([ video, captionsResult ]) => { |
119 | const startTime = this.route.snapshot.queryParams.start | 116 | const startTime = this.route.snapshot.queryParams.start |
120 | const subtitle = this.route.snapshot.queryParams.subtitle | 117 | const subtitle = this.route.snapshot.queryParams.subtitle |
118 | const playerMode = this.route.snapshot.queryParams.mode | ||
121 | 119 | ||
122 | this.onVideoFetched(video, captionsResult.data, { startTime, subtitle }) | 120 | this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode }) |
123 | .catch(err => this.handleError(err)) | 121 | .catch(err => this.handleError(err)) |
124 | }) | 122 | }) |
125 | }) | 123 | }) |
@@ -366,7 +364,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
366 | ) | 364 | ) |
367 | } | 365 | } |
368 | 366 | ||
369 | private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], urlOptions: { startTime: number, subtitle: string }) { | 367 | private async onVideoFetched ( |
368 | video: VideoDetails, | ||
369 | videoCaptions: VideoCaption[], | ||
370 | urlOptions: { startTime?: number, subtitle?: string, playerMode?: string } | ||
371 | ) { | ||
370 | this.video = video | 372 | this.video = video |
371 | 373 | ||
372 | // Re init attributes | 374 | // Re init attributes |
@@ -402,41 +404,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
402 | src: environment.apiUrl + c.captionPath | 404 | src: environment.apiUrl + c.captionPath |
403 | })) | 405 | })) |
404 | 406 | ||
405 | const videojsOptions = getVideojsOptions({ | 407 | const options: PeertubePlayerManagerOptions = { |
406 | autoplay: this.isAutoplay(), | 408 | common: { |
407 | inactivityTimeout: 2500, | 409 | autoplay: this.isAutoplay(), |
408 | videoFiles: this.video.files, | 410 | |
409 | videoCaptions: playerCaptions, | 411 | playerElement: this.playerElement, |
410 | playerElement: this.playerElement, | 412 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, |
411 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, | 413 | |
412 | videoDuration: this.video.duration, | 414 | videoDuration: this.video.duration, |
413 | enableHotkeys: true, | 415 | enableHotkeys: true, |
414 | peertubeLink: false, | 416 | inactivityTimeout: 2500, |
415 | poster: this.video.previewUrl, | 417 | poster: this.video.previewUrl, |
416 | startTime, | 418 | startTime, |
417 | subtitle: urlOptions.subtitle, | 419 | |
418 | theaterMode: true, | 420 | theaterMode: true, |
419 | language: this.localeId, | 421 | captions: videoCaptions.length !== 0, |
420 | 422 | peertubeLink: false, | |
421 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | 423 | |
422 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | 424 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, |
423 | authorizationHeader: this.authService.getRequestHeaderValue() | 425 | embedUrl: this.video.embedUrl, |
424 | } : undefined | 426 | |
425 | }) | 427 | language: this.localeId, |
428 | |||
429 | subtitle: urlOptions.subtitle, | ||
430 | |||
431 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | ||
432 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | ||
433 | authorizationHeader: this.authService.getRequestHeaderValue() | ||
434 | } : undefined, | ||
435 | |||
436 | serverUrl: environment.apiUrl, | ||
426 | 437 | ||
427 | if (this.videojsLocaleLoaded === false) { | 438 | videoCaptions: playerCaptions |
428 | await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) | 439 | }, |
429 | this.videojsLocaleLoaded = true | 440 | |
441 | webtorrent: { | ||
442 | videoFiles: this.video.files | ||
443 | } | ||
430 | } | 444 | } |
431 | 445 | ||
432 | const self = this | 446 | const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' |
433 | this.zone.runOutsideAngular(async () => { | 447 | |
434 | videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { | 448 | if (mode === 'p2p-media-loader') { |
435 | self.player = this | 449 | const hlsPlaylist = this.video.getHlsPlaylist() |
436 | this.on('customError', ({ err }: { err: any }) => self.handleError(err)) | ||
437 | 450 | ||
438 | addContextMenu(self.player, self.video.embedUrl) | 451 | const p2pMediaLoader = { |
439 | }) | 452 | playlistUrl: hlsPlaylist.playlistUrl, |
453 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
454 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
455 | trackerAnnounce: this.video.trackerUrls, | ||
456 | videoFiles: this.video.files | ||
457 | } as P2PMediaLoaderOptions | ||
458 | |||
459 | Object.assign(options, { p2pMediaLoader }) | ||
460 | } | ||
461 | |||
462 | this.zone.runOutsideAngular(async () => { | ||
463 | this.player = await PeertubePlayerManager.initialize(mode, options) | ||
464 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | ||
440 | }) | 465 | }) |
441 | 466 | ||
442 | this.setVideoDescriptionHTML() | 467 | this.setVideoDescriptionHTML() |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index 881ab2174..6fd74e67a 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -39,18 +39,21 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
39 | 39 | ||
40 | this.generateSyndicationList() | 40 | this.generateSyndicationList() |
41 | 41 | ||
42 | const trendingDays = this.serverService.getConfig().trending.videos.intervalDays | 42 | this.serverService.configLoaded.subscribe( |
43 | () => { | ||
44 | const trendingDays = this.serverService.getConfig().trending.videos.intervalDays | ||
43 | 45 | ||
44 | if (trendingDays === 1) { | 46 | if (trendingDays === 1) { |
45 | this.titlePage = this.i18n('Trending for the last 24 hours') | 47 | this.titlePage = this.i18n('Trending for the last 24 hours') |
46 | this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.') | 48 | this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.') |
47 | } else { | 49 | } else { |
48 | this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays }) | 50 | this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays }) |
49 | this.titleTooltip = this.i18n( | 51 | this.titleTooltip = this.i18n( |
50 | 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.', | 52 | 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.', |
51 | { days: trendingDays } | 53 | { days: trendingDays } |
52 | ) | 54 | ) |
53 | } | 55 | } |
56 | }) | ||
54 | } | 57 | } |
55 | 58 | ||
56 | ngOnDestroy () { | 59 | ngOnDestroy () { |
diff --git a/client/src/assets/images/global/add.svg b/client/src/assets/images/global/add.html index 42b269c43..bfb0a52bc 100644 --- a/client/src/assets/images/global/add.svg +++ b/client/src/assets/images/global/add.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-92.000000, -115.000000)"> |
5 | <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> | ||
6 | <g id="2" transform="translate(92.000000, 115.000000)"> | 4 | <g id="2" transform="translate(92.000000, 115.000000)"> |
7 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> |
8 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> | 6 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> |
diff --git a/client/src/assets/images/video/alert.svg b/client/src/assets/images/global/alert.html index 5b43534ad..7c8c02074 100644 --- a/client/src/assets/images/video/alert.svg +++ b/client/src/assets/images/global/alert.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>alert</title> | 3 | <g transform="translate(-48.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -467.000000)"> | ||
9 | <g id="161" transform="translate(48.000000, 467.000000)"> | 4 | <g id="161" transform="translate(48.000000, 467.000000)"> |
10 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> | 5 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> | 6 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> |
diff --git a/client/src/assets/images/global/circle-tick.html b/client/src/assets/images/global/circle-tick.html new file mode 100644 index 000000000..2327de6be --- /dev/null +++ b/client/src/assets/images/global/circle-tick.html | |||
@@ -0,0 +1,12 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
5 | <g id="yes" transform="translate(352.000000, 88.000000)"> | ||
6 | <circle id="Oval-1" cx="12" cy="12" r="10"/> | ||
7 | <polyline id="Path-288" stroke-linecap="round" stroke-linejoin="round" points="8.5 12.5 10.5 14.5 15.5 9.5"/> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </g> | ||
12 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-download.html b/client/src/assets/images/global/cloud-download.html new file mode 100644 index 000000000..b2634fd1f --- /dev/null +++ b/client/src/assets/images/global/cloud-download.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
3 | <g transform="translate(-356.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="308" transform="translate(356.000000, 775.000000)"> | ||
5 | <path d="M8,17 L5,17 L5,17 C2.790861,17 1,15.209139 1,13 C1,10.790861 2.790861,9 5,9 C5.35840468,9 5.70579988,9.04713713 6.03632437,9.13555013 C6.01233106,8.92702603 6,8.71495305 6,8.5 C6,5.46243388 8.46243388,3 11.5,3 C14.0673313,3 16.2238156,4.7590449 16.8299648,7.1376465 C17.2052921,7.04765874 17.5970804,7 18,7 C20.7614237,7 23,9.23857625 23,12 C23,14.7614237 20.7614237,17 18,17 L16,17" id="Combined-Shape" stroke-linejoin="round"></path> | ||
6 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
7 | <polyline id="Path-59" stroke-linejoin="round" points="15 20 12 23 9 20"></polyline> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-error.html b/client/src/assets/images/global/cloud-error.html new file mode 100644 index 000000000..1a3483805 --- /dev/null +++ b/client/src/assets/images/global/cloud-error.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-400.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="309" transform="translate(400.000000, 775.000000)"> | ||
5 | <path d="M7,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L17,18" id="Combined-Shape"></path> | ||
6 | <path d="M9,21 L15,15" id="Path-238"></path> | ||
7 | <path d="M9,21 L15,15" id="Path-238" transform="translate(12.000000, 18.000000) scale(-1, 1) translate(-12.000000, -18.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cog.html b/client/src/assets/images/global/cog.html new file mode 100644 index 000000000..b74a180e7 --- /dev/null +++ b/client/src/assets/images/global/cog.html | |||
@@ -0,0 +1,9 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round"> | ||
3 | <g transform="translate(-796.000000, -159.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="38" transform="translate(796.000000, 159.000000)"> | ||
5 | <path d="M7.20852293,4.3800958 C8.05442158,3.84706631 8.99528987,3.45099725 10,3.22301642 L10,1.99980749 C10,1.44762906 10.4433532,1 11.0093689,1 L12.9906311,1 C13.5480902,1 14,1.44371665 14,1.99980749 L14,3.22301642 C15.0047101,3.45099725 15.9455784,3.84706631 16.7914771,4.3800958 L17.6569904,3.5145825 C18.0474395,3.12413339 18.6774591,3.12110988 19.0776926,3.52134344 L20.4786566,4.92230738 C20.8728396,5.31649045 20.8786331,5.94979402 20.4854175,6.34300963 L19.6199042,7.20852293 C20.1529337,8.05442158 20.5490027,8.99528987 20.7769836,10 L22.0001925,10 C22.5523709,10 23,10.4433532 23,11.0093689 L23,12.9906311 C23,13.5480902 22.5562834,14 22.0001925,14 L20.7769836,14 C20.5490027,15.0047101 20.1529337,15.9455784 19.6199042,16.7914771 L20.4854175,17.6569904 C20.8758666,18.0474395 20.8788901,18.6774591 20.4786566,19.0776926 L19.0776926,20.4786566 C18.6835095,20.8728396 18.050206,20.8786331 17.6569904,20.4854175 L16.7914771,19.6199042 C15.9455784,20.1529337 15.0047101,20.5490027 14,20.7769836 L14,22.0001925 C14,22.5523709 13.5566468,23 12.9906311,23 L11.0093689,23 C10.4519098,23 10,22.5562834 10,22.0001925 L10,20.7769836 C8.99528987,20.5490027 8.05442158,20.1529337 7.20852293,19.6199042 L6.34300963,20.4854175 C5.95256051,20.8758666 5.32254093,20.8788901 4.92230738,20.4786566 L3.52134344,19.0776926 C3.12716036,18.6835095 3.12136689,18.050206 3.5145825,17.6569904 L4.3800958,16.7914771 C3.84706631,15.9455784 3.45099725,15.0047101 3.22301642,14 L1.99980749,14 C1.44762906,14 1,13.5566468 1,12.9906311 L1,11.0093689 C1,10.4519098 1.44371665,10 1.99980749,10 L3.22301642,10 C3.45099725,8.99528987 3.84706631,8.05442158 4.3800958,7.20852293 L3.5145825,6.34300963 C3.12413339,5.95256051 3.12110988,5.32254093 3.52134344,4.92230738 L4.92230738,3.52134344 C5.31649045,3.12716036 5.94979402,3.12136689 6.34300963,3.5145825 L7.20852293,4.3800958 Z M12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 Z" id="Combined-Shape"/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/cross.svg b/client/src/assets/images/global/cross.html index d47a75996..962578487 100644 --- a/client/src/assets/images/global/cross.svg +++ b/client/src/assets/images/global/cross.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-312.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-312.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="7" transform="translate(312.000000, 115.000000)"> | 4 | <g id="7" transform="translate(312.000000, 115.000000)"> |
7 | <path d="M19,5 L5,19" id="Path-14"></path> | 5 | <path d="M19,5 L5,19" id="Path-14"></path> |
8 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> | 6 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> |
diff --git a/client/src/assets/images/global/delete-black.svg b/client/src/assets/images/global/delete-black.svg deleted file mode 100644 index 04ddc23aa..000000000 --- a/client/src/assets/images/global/delete-black.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#000" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-grey.svg b/client/src/assets/images/global/delete-grey.svg deleted file mode 100644 index 67e9e2ce7..000000000 --- a/client/src/assets/images/global/delete-grey.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#585858" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#585858" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#585858"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-white.svg b/client/src/assets/images/global/delete.html index 9c52de557..a0d9a0cac 100644 --- a/client/src/assets/images/global/delete-white.svg +++ b/client/src/assets/images/global/delete.html | |||
@@ -1,13 +1,11 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-224.000000, -159.000000)"> |
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | 4 | <g id="25" transform="translate(224.000000, 159.000000)"> |
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#ffffff" stroke-width="2"></path> | 5 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000000" stroke-width="2"></path> |
8 | <rect id="Rectangle-424" fill="#ffffff" x="2" y="4" width="20" height="2" rx="1"></rect> | 6 | <rect id="Rectangle-424" fill="#000000" x="2" y="4" width="20" height="2" rx="1"></rect> |
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#ffffff"></path> | 7 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000000"></path> |
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"></path> | 8 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | </g> | 9 | </g> |
12 | </g> | 10 | </g> |
13 | </g> | 11 | </g> |
diff --git a/client/src/assets/images/video/download-black.svg b/client/src/assets/images/global/download.html index 501836746..259506f31 100644 --- a/client/src/assets/images/video/download-black.svg +++ b/client/src/assets/images/global/download.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>download</title> | 3 | <g transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | 4 | <g id="84" transform="translate(180.000000, 291.000000)"> |
10 | <path d="M12,3 L12,15" id="Path-58"></path> | 5 | <path d="M12,3 L12,15" id="Path-58"></path> |
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | 6 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> |
diff --git a/client/src/assets/images/global/edit-black.svg b/client/src/assets/images/global/edit-black.svg deleted file mode 100644 index 0176b0f37..000000000 --- a/client/src/assets/images/global/edit-black.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>edit</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | ||
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | ||
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/global/edit-grey.svg b/client/src/assets/images/global/edit.html index 23ece68f1..f04183c2d 100644 --- a/client/src/assets/images/global/edit-grey.svg +++ b/client/src/assets/images/global/edit.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>edit</title> | 3 | <g transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | 4 | <g id="41" transform="translate(48.000000, 203.000000)"> |
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | 5 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> |
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | 6 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> |
diff --git a/client/src/assets/images/global/help.svg b/client/src/assets/images/global/help.html index 48252febe..80cd40321 100644 --- a/client/src/assets/images/global/help.svg +++ b/client/src/assets/images/global/help.html | |||
@@ -1,12 +1,10 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -247.000000)"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -247.000000)"> | ||
6 | <g id="69" transform="translate(400.000000, 247.000000)"> | 4 | <g id="69" transform="translate(400.000000, 247.000000)"> |
7 | <circle id="Oval-7" stroke="#333333" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <circle id="Oval-7" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle> |
8 | <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#333333"></path> | 6 | <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#000000"></path> |
9 | </g> | 7 | </g> |
10 | </g> | 8 | </g> |
11 | </g> | 9 | </g> |
12 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/im-with-her.svg b/client/src/assets/images/global/im-with-her.html index 31d4754fd..de2c62e96 100644 --- a/client/src/assets/images/global/im-with-her.svg +++ b/client/src/assets/images/global/im-with-her.html | |||
@@ -1,15 +1,10 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>im-with-her</title> | 3 | <g transform="translate(-708.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -467.000000)"> | ||
9 | <g id="176" transform="translate(708.000000, 467.000000)"> | 4 | <g id="176" transform="translate(708.000000, 467.000000)"> |
10 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#333333" opacity="0.5"></path> | 5 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#000000" opacity="0.5"></path> |
11 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> | 6 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> |
12 | </g> | 7 | </g> |
13 | </g> | 8 | </g> |
14 | </g> | 9 | </g> |
15 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/no.html b/client/src/assets/images/global/no.html new file mode 100644 index 000000000..bb7b28514 --- /dev/null +++ b/client/src/assets/images/global/no.html | |||
@@ -0,0 +1,10 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
5 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
6 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
7 | </g> | ||
8 | </g> | ||
9 | </g> | ||
10 | </svg> | ||
diff --git a/client/src/assets/images/global/sparkle.html b/client/src/assets/images/global/sparkle.html new file mode 100644 index 000000000..3b29fefb9 --- /dev/null +++ b/client/src/assets/images/global/sparkle.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-488.000000, -731.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="291" transform="translate(488.000000, 731.000000)"> | ||
5 | <path d="M10,9 C8.5,7.5 8,3 8,3 C8,3 7.5,7.5 6,9 C4.5,10.5 2,11 2,11 C2,11 4.5,11.5 6,13 C7.5,14.5 8,19 8,19 C8,19 8.5,14.5 10,13 C11.5,11.5 14,11 14,11 C14,11 11.5,10.5 10,9 Z" id="Combined-Shape"></path> | ||
6 | <path d="M19.6666667,4.75 C18.7916667,3.8125 18.5,1 18.5,1 C18.5,1 18.2083333,3.8125 17.3333333,4.75 C16.4583333,5.6875 15,6 15,6 C15,6 16.4583333,6.3125 17.3333333,7.25 C18.2083333,8.1875 18.5,11 18.5,11 C18.5,11 18.7916667,8.1875 19.6666667,7.25 C20.5416667,6.3125 22,6 22,6 C22,6 20.5416667,5.6875 19.6666667,4.75 Z" id="Combined-Shape"></path> | ||
7 | <path d="M17,17 C16.25,16.25 16,14 16,14 C16,14 15.75,16.25 15,17 C14.25,17.75 13,18 13,18 C13,18 14.25,18.25 15,19 C15.75,19.75 16,22 16,22 C16,22 16.25,19.75 17,19 C17.75,18.25 19,18 19,18 C19,18 17.75,17.75 17,17 Z" id="Combined-Shape"></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/syndication.svg b/client/src/assets/images/global/syndication.html index cb74cf81b..e6c88a4db 100644 --- a/client/src/assets/images/global/syndication.svg +++ b/client/src/assets/images/global/syndication.html | |||
@@ -1,10 +1,8 @@ | |||
1 | <?xml version="1.0" encoding="iso-8859-1"?> | ||
2 | <!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | ||
3 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | 1 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" |
4 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> | 2 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> |
5 | <g> | 3 | <g> |
6 | <g> | 4 | <g> |
7 | <path style="fill:#010002;" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 | 5 | <path fill="#000000" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 |
8 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 | 6 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 |
9 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 | 7 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 |
10 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 | 8 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 |
diff --git a/client/src/assets/images/global/tick.svg b/client/src/assets/images/global/tick.html index 230caa111..4784b4807 100644 --- a/client/src/assets/images/global/tick.svg +++ b/client/src/assets/images/global/tick.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-356.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="8" transform="translate(356.000000, 115.000000)"> | 4 | <g id="8" transform="translate(356.000000, 115.000000)"> |
7 | <path d="M21,6 L9,18" id="Path-14"></path> | 5 | <path d="M21,6 L9,18" id="Path-14"></path> |
8 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> | 6 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> |
diff --git a/client/src/assets/images/global/undo.html b/client/src/assets/images/global/undo.html new file mode 100644 index 000000000..228245c86 --- /dev/null +++ b/client/src/assets/images/global/undo.html | |||
@@ -0,0 +1,9 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-180.000000, -115.000000)" fill="#000000"> | ||
4 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
5 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/undo.svg b/client/src/assets/images/global/undo.svg deleted file mode 100644 index f1cca03f7..000000000 --- a/client/src/assets/images/global/undo.svg +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-180.000000, -115.000000)" fill="#000"> | ||
6 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
7 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/user-add.html b/client/src/assets/images/global/user-add.html new file mode 100644 index 000000000..57df23c74 --- /dev/null +++ b/client/src/assets/images/global/user-add.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-136.000000, -863.000000)"> | ||
4 | <g id="343" transform="translate(136.000000, 863.000000)"> | ||
5 | <path d="M14.2571621,15 L7,15 C4.20063223,15 2.390348,16.1679253 1.5255785,18.0896353 C1.07423388,19.0926234 0.949016905,20.1108713 0.995546634,20.9698816 C0.998604759,21.0263393 1.0014872,21.0632937 1.00496281,21.0995037 C1.0599172,21.6490476 1.54995985,22.0499916 2.09950372,21.9950372 C2.64904758,21.9400828 3.04999158,21.4500401 2.99503719,20.9004963 C2.99555422,20.9071205 2.99399879,20.8871791 2.99261905,20.8617069 C2.96185588,20.2937714 3.05021139,19.575276 3.34942151,18.9103647 C3.890902,17.7070747 4.98686778,17 7,17 L12.0070975,17 L13.2070325,17 C13.4170071,16.2576107 13.7789623,15.5790321 14.2571621,15 Z" id="Path-41" fill="#000000" fill-rule="nonzero"></path> | ||
6 | <path d="M19,18 L19,16.4976988 C19,16.2228273 18.7680664,16 18.5,16 C18.2238576,16 18,16.2148438 18,16.4976988 L18,18 L16.4976988,18 C16.2148438,18 16,18.2238576 16,18.5 C16,18.7680664 16.2228273,19 16.4976988,19 L18,19 L18,20.5023012 C18,20.7771727 18.2319336,21 18.5,21 C18.7761424,21 19,20.7851562 19,20.5023012 L19,19 L20.5023012,19 C20.7851562,19 21,18.7761424 21,18.5 C21,18.2319336 20.7771727,18 20.5023012,18 L19,18 Z M18.5,23 C16.0147186,23 14,20.9852814 14,18.5 C14,16.0147186 16.0147186,14 18.5,14 C20.9852814,14 23,16.0147186 23,18.5 C23,20.9852814 20.9852814,23 18.5,23 Z" id="Combined-Shape" fill="#000000"></path> | ||
7 | <circle id="Oval-40" stroke="#000000" stroke-width="2" cx="12" cy="8" r="5"></circle> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/validate.svg b/client/src/assets/images/global/validate.html index 5c7ee9d14..520624ff6 100644 --- a/client/src/assets/images/global/validate.svg +++ b/client/src/assets/images/global/validate.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -1134.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="yes" transform="translate(352.000000, 88.000000)"> | 5 | <g id="yes" transform="translate(352.000000, 88.000000)"> |
8 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> | 6 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> |
diff --git a/client/src/assets/images/video/blacklist.svg b/client/src/assets/images/video/blacklist.svg deleted file mode 100644 index 431c73816..000000000 --- a/client/src/assets/images/video/blacklist.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>no</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
10 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
11 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-white.svg b/client/src/assets/images/video/dislike-white.svg deleted file mode 100644 index cfc6eaa1f..000000000 --- a/client/src/assets/images/video/dislike-white.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | ||
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | ||
9 | <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-grey.svg b/client/src/assets/images/video/dislike.html index 56a7908fb..acde951e2 100644 --- a/client/src/assets/images/video/dislike-grey.svg +++ b/client/src/assets/images/video/dislike.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-752.000000, -1090.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | 5 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> |
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | 6 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> |
diff --git a/client/src/assets/images/video/download-grey.svg b/client/src/assets/images/video/download-grey.svg deleted file mode 100644 index 5b0cca5ef..000000000 --- a/client/src/assets/images/video/download-grey.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/download-white.svg b/client/src/assets/images/video/download-white.svg deleted file mode 100644 index 0e66e06e8..000000000 --- a/client/src/assets/images/video/download-white.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/heart.svg b/client/src/assets/images/video/heart.html index 5d64aee0f..618f64f10 100644 --- a/client/src/assets/images/video/heart.svg +++ b/client/src/assets/images/video/heart.html | |||
@@ -1,9 +1,7 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#585858"> | 4 | <g transform="translate(48.000000, 1046.000000)"> |
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="heart"> | 5 | <g id="heart"> |
8 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> | 6 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> |
9 | </g> | 7 | </g> |
diff --git a/client/src/assets/images/video/like-white.svg b/client/src/assets/images/video/like-white.svg deleted file mode 100644 index 88e5f6a9a..000000000 --- a/client/src/assets/images/video/like-white.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>thumbs-up</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | ||
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | ||
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/like-grey.svg b/client/src/assets/images/video/like.html index 5ef6c7b31..d0e71763b 100644 --- a/client/src/assets/images/video/like-grey.svg +++ b/client/src/assets/images/video/like.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>thumbs-up</title> | 3 | <g transform="translate(-708.000000, -643.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | 4 | <g id="256" transform="translate(708.000000, 643.000000)"> |
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | 5 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> |
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | 6 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> |
diff --git a/client/src/assets/images/video/more.svg b/client/src/assets/images/video/more.html index dea392136..39dcad10e 100644 --- a/client/src/assets/images/video/more.svg +++ b/client/src/assets/images/video/more.html | |||
@@ -1,8 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-444.000000, -115.000000)" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-444.000000, -115.000000)" fill="#585858"> | ||
6 | <g id="10" transform="translate(444.000000, 115.000000)"> | 4 | <g id="10" transform="translate(444.000000, 115.000000)"> |
7 | <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> | 5 | <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> |
8 | </g> | 6 | </g> |
diff --git a/client/src/assets/images/video/share.svg b/client/src/assets/images/video/share.html index da0f43e81..7759b37af 100644 --- a/client/src/assets/images/video/share.svg +++ b/client/src/assets/images/video/share.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>share</title> | 3 | <g transform="translate(-312.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="47" transform="translate(312.000000, 203.000000)"> | 4 | <g id="47" transform="translate(312.000000, 203.000000)"> |
10 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> | 5 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> |
11 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> | 6 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> |
diff --git a/client/src/assets/images/header/upload-white.svg b/client/src/assets/images/video/upload.html index 2b07caf76..3bc0d3a8a 100644 --- a/client/src/assets/images/header/upload-white.svg +++ b/client/src/assets/images/video/upload.html | |||
@@ -1,11 +1,6 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | 1 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>cloud-upload</title> | 3 | <g transform="translate(-312.000000, -775.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | 4 | <g id="307" transform="translate(312.000000, 775.000000)"> |
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | 5 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> |
11 | <path d="M12,13 L12,21" id="Path-58"></path> | 6 | <path d="M12,13 L12,21" id="Path-58"></path> |
diff --git a/client/src/assets/images/video/upload.svg b/client/src/assets/images/video/upload.svg deleted file mode 100644 index c5b7cb443..000000000 --- a/client/src/assets/images/video/upload.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>cloud-upload</title> | ||
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#C6C6C6" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | ||
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | ||
11 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
12 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
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 new file mode 100644 index 000000000..022a9c16f --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,143 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
6 | import { Events } from 'p2p-media-loader-core' | ||
7 | |||
8 | // videojs-hlsjs-plugin needs videojs in window | ||
9 | window['videojs'] = videojs | ||
10 | require('@streamroot/videojs-hlsjs-plugin') | ||
11 | |||
12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
13 | class P2pMediaLoaderPlugin extends Plugin { | ||
14 | |||
15 | private readonly CONSTANTS = { | ||
16 | INFO_SCHEDULER: 1000 // Don't change this | ||
17 | } | ||
18 | private readonly options: P2PMediaLoaderPluginOptions | ||
19 | |||
20 | private hlsjs: any // Don't type hlsjs to not bundle the module | ||
21 | private p2pEngine: Engine | ||
22 | private statsP2PBytes = { | ||
23 | pendingDownload: [] as number[], | ||
24 | pendingUpload: [] as number[], | ||
25 | numPeers: 0, | ||
26 | totalDownload: 0, | ||
27 | totalUpload: 0 | ||
28 | } | ||
29 | private statsHTTPBytes = { | ||
30 | pendingDownload: [] as number[], | ||
31 | pendingUpload: [] as number[], | ||
32 | totalDownload: 0, | ||
33 | totalUpload: 0 | ||
34 | } | ||
35 | |||
36 | private networkInfoInterval: any | ||
37 | |||
38 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | ||
39 | super(player, options) | ||
40 | |||
41 | this.options = options | ||
42 | |||
43 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
44 | this.hlsjs = hlsjs | ||
45 | }) | ||
46 | |||
47 | initVideoJsContribHlsJsPlayer(player) | ||
48 | |||
49 | player.src({ | ||
50 | type: options.type, | ||
51 | src: options.src | ||
52 | }) | ||
53 | |||
54 | player.on('play', () => { | ||
55 | player.addClass('vjs-has-big-play-button-clicked') | ||
56 | }) | ||
57 | |||
58 | player.ready(() => this.initialize()) | ||
59 | } | ||
60 | |||
61 | dispose () { | ||
62 | if (this.hlsjs) this.hlsjs.destroy() | ||
63 | if (this.p2pEngine) this.p2pEngine.destroy() | ||
64 | |||
65 | clearInterval(this.networkInfoInterval) | ||
66 | } | ||
67 | |||
68 | private initialize () { | ||
69 | initHlsJsPlayer(this.hlsjs) | ||
70 | |||
71 | const tech = this.player.tech_ | ||
72 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | ||
73 | |||
74 | // Avoid using constants to not import hls.hs | ||
75 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
76 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
77 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | ||
78 | }) | ||
79 | |||
80 | this.p2pEngine.on(Events.SegmentError, (segment, err) => { | ||
81 | console.error('Segment error.', segment, err) | ||
82 | }) | ||
83 | |||
84 | this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length | ||
85 | |||
86 | this.runStats() | ||
87 | } | ||
88 | |||
89 | private runStats () { | ||
90 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | ||
91 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
92 | |||
93 | elem.pendingDownload.push(size) | ||
94 | elem.totalDownload += size | ||
95 | }) | ||
96 | |||
97 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | ||
98 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
99 | |||
100 | elem.pendingUpload.push(size) | ||
101 | elem.totalUpload += size | ||
102 | }) | ||
103 | |||
104 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | ||
105 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | ||
106 | |||
107 | this.networkInfoInterval = setInterval(() => { | ||
108 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | ||
109 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) | ||
110 | |||
111 | const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) | ||
112 | const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) | ||
113 | |||
114 | this.statsP2PBytes.pendingDownload = [] | ||
115 | this.statsP2PBytes.pendingUpload = [] | ||
116 | this.statsHTTPBytes.pendingDownload = [] | ||
117 | this.statsHTTPBytes.pendingUpload = [] | ||
118 | |||
119 | return this.player.trigger('p2pInfo', { | ||
120 | http: { | ||
121 | downloadSpeed: httpDownloadSpeed, | ||
122 | uploadSpeed: httpUploadSpeed, | ||
123 | downloaded: this.statsHTTPBytes.totalDownload, | ||
124 | uploaded: this.statsHTTPBytes.totalUpload | ||
125 | }, | ||
126 | p2p: { | ||
127 | downloadSpeed: p2pDownloadSpeed, | ||
128 | uploadSpeed: p2pUploadSpeed, | ||
129 | numPeers: this.statsP2PBytes.numPeers, | ||
130 | downloaded: this.statsP2PBytes.totalDownload, | ||
131 | uploaded: this.statsP2PBytes.totalUpload | ||
132 | } | ||
133 | } as PlayerNetworkInfo) | ||
134 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
135 | } | ||
136 | |||
137 | private arraySum (data: number[]) { | ||
138 | return data.reduce((a: number, b: number) => a + b, 0) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
143 | export { P2pMediaLoaderPlugin } | ||
diff --git a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts new file mode 100644 index 000000000..32e7ce4f2 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { Segment } from 'p2p-media-loader-core' | ||
3 | |||
4 | function segmentUrlBuilderFactory (baseUrls: string[]) { | ||
5 | return function segmentBuilder (segment: Segment) { | ||
6 | const max = baseUrls.length + 1 | ||
7 | const i = getRandomInt(max) | ||
8 | |||
9 | if (i === max - 1) return segment.url | ||
10 | |||
11 | let newBaseUrl = baseUrls[i] | ||
12 | let middlePart = newBaseUrl.endsWith('/') ? '' : '/' | ||
13 | |||
14 | return newBaseUrl + middlePart + basename(segment.url) | ||
15 | } | ||
16 | } | ||
17 | |||
18 | // --------------------------------------------------------------------------- | ||
19 | |||
20 | export { | ||
21 | segmentUrlBuilderFactory | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function getRandomInt (max: number) { | ||
27 | return Math.floor(Math.random() * Math.floor(max)) | ||
28 | } | ||
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts new file mode 100644 index 000000000..72c32f9e0 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | ||
2 | import { basename } from 'path' | ||
3 | |||
4 | function segmentValidatorFactory (segmentsSha256Url: string) { | ||
5 | const segmentsJSON = fetchSha256Segments(segmentsSha256Url) | ||
6 | const regex = /bytes=(\d+)-(\d+)/ | ||
7 | |||
8 | return async function segmentValidator (segment: Segment) { | ||
9 | const filename = basename(segment.url) | ||
10 | const captured = regex.exec(segment.range) | ||
11 | |||
12 | const range = captured[1] + '-' + captured[2] | ||
13 | |||
14 | const hashShouldBe = (await segmentsJSON)[filename][range] | ||
15 | if (hashShouldBe === undefined) { | ||
16 | throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) | ||
17 | } | ||
18 | |||
19 | const calculatedSha = bufferToEx(await sha256(segment.data)) | ||
20 | if (calculatedSha !== hashShouldBe) { | ||
21 | throw new Error( | ||
22 | `Hashes does not correspond for segment ${filename}/${range}` + | ||
23 | `(expected: ${hashShouldBe} instead of ${calculatedSha})` | ||
24 | ) | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | segmentValidatorFactory | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function fetchSha256Segments (url: string) { | ||
38 | return fetch(url) | ||
39 | .then(res => res.json()) | ||
40 | .catch(err => { | ||
41 | console.error('Cannot get sha256 segments', err) | ||
42 | return {} | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | function sha256 (data?: ArrayBuffer) { | ||
47 | if (!data) return undefined | ||
48 | |||
49 | return window.crypto.subtle.digest('SHA-256', data) | ||
50 | } | ||
51 | |||
52 | // Thanks: https://stackoverflow.com/a/53307879 | ||
53 | function bufferToEx (buffer?: ArrayBuffer) { | ||
54 | if (!buffer) return '' | ||
55 | |||
56 | let s = '' | ||
57 | const h = '0123456789abcdef' | ||
58 | const o = new Uint8Array(buffer) | ||
59 | |||
60 | o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ]) | ||
61 | |||
62 | return s | ||
63 | } | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts new file mode 100644 index 000000000..0ba9bcb11 --- /dev/null +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -0,0 +1,466 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | ||
5 | import 'videojs-dock' | ||
6 | import 'videojs-contextmenu-ui' | ||
7 | import 'videojs-contrib-quality-levels' | ||
8 | import './peertube-plugin' | ||
9 | import './videojs-components/peertube-link-button' | ||
10 | import './videojs-components/resolution-menu-button' | ||
11 | import './videojs-components/settings-menu-button' | ||
12 | import './videojs-components/p2p-info-button' | ||
13 | import './videojs-components/peertube-load-progress-bar' | ||
14 | import './videojs-components/theater-button' | ||
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | ||
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | ||
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
18 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
19 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
25 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
29 | |||
30 | export type WebtorrentOptions = { | ||
31 | videoFiles: VideoFile[] | ||
32 | } | ||
33 | |||
34 | export type P2PMediaLoaderOptions = { | ||
35 | playlistUrl: string | ||
36 | segmentsSha256Url: string | ||
37 | trackerAnnounce: string[] | ||
38 | redundancyBaseUrls: string[] | ||
39 | videoFiles: VideoFile[] | ||
40 | } | ||
41 | |||
42 | export type CommonOptions = { | ||
43 | playerElement: HTMLVideoElement | ||
44 | onPlayerElementChange: (element: HTMLVideoElement) => void | ||
45 | |||
46 | autoplay: boolean | ||
47 | videoDuration: number | ||
48 | enableHotkeys: boolean | ||
49 | inactivityTimeout: number | ||
50 | poster: string | ||
51 | startTime: number | string | ||
52 | |||
53 | theaterMode: boolean | ||
54 | captions: boolean | ||
55 | peertubeLink: boolean | ||
56 | |||
57 | videoViewUrl: string | ||
58 | embedUrl: string | ||
59 | |||
60 | language?: string | ||
61 | controls?: boolean | ||
62 | muted?: boolean | ||
63 | loop?: boolean | ||
64 | subtitle?: string | ||
65 | |||
66 | videoCaptions: VideoJSCaption[] | ||
67 | |||
68 | userWatching?: UserWatching | ||
69 | |||
70 | serverUrl: string | ||
71 | } | ||
72 | |||
73 | export type PeertubePlayerManagerOptions = { | ||
74 | common: CommonOptions, | ||
75 | webtorrent: WebtorrentOptions, | ||
76 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
77 | } | ||
78 | |||
79 | export class PeertubePlayerManager { | ||
80 | |||
81 | private static videojsLocaleCache: { [ path: string ]: any } = {} | ||
82 | private static playerElementClassName: string | ||
83 | |||
84 | static getServerTranslations (serverUrl: string, locale: string) { | ||
85 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
86 | // It is the default locale, nothing to translate | ||
87 | if (!path) return Promise.resolve(undefined) | ||
88 | |||
89 | return fetch(path + '/server.json') | ||
90 | .then(res => res.json()) | ||
91 | .catch(err => { | ||
92 | console.error('Cannot get server translations', err) | ||
93 | return undefined | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | ||
98 | let p2pMediaLoader: any | ||
99 | |||
100 | this.playerElementClassName = options.common.playerElement.className | ||
101 | |||
102 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') | ||
103 | if (mode === 'p2p-media-loader') { | ||
104 | [ p2pMediaLoader ] = await Promise.all([ | ||
105 | import('p2p-media-loader-hlsjs'), | ||
106 | import('./p2p-media-loader/p2p-media-loader-plugin') | ||
107 | ]) | ||
108 | } | ||
109 | |||
110 | const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader) | ||
111 | |||
112 | await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language) | ||
113 | |||
114 | const self = this | ||
115 | return new Promise(res => { | ||
116 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | ||
117 | const player = this | ||
118 | |||
119 | player.tech_.on('error', () => { | ||
120 | // Fallback to webtorrent? | ||
121 | if (mode === 'p2p-media-loader') { | ||
122 | self.fallbackToWebTorrent(player, options) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
127 | |||
128 | return res(player) | ||
129 | }) | ||
130 | }) | ||
131 | } | ||
132 | |||
133 | private static async fallbackToWebTorrent (player: any, options: PeertubePlayerManagerOptions) { | ||
134 | const newVideoElement = document.createElement('video') | ||
135 | newVideoElement.className = this.playerElementClassName | ||
136 | |||
137 | // VideoJS wraps our video element inside a div | ||
138 | const currentParentPlayerElement = options.common.playerElement.parentNode | ||
139 | currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement) | ||
140 | |||
141 | options.common.playerElement = newVideoElement | ||
142 | options.common.onPlayerElementChange(newVideoElement) | ||
143 | |||
144 | player.dispose() | ||
145 | |||
146 | await import('./webtorrent/webtorrent-plugin') | ||
147 | |||
148 | const mode = 'webtorrent' | ||
149 | const videojsOptions = this.getVideojsOptions(mode, options) | ||
150 | |||
151 | const self = this | ||
152 | videojs(newVideoElement, videojsOptions, function (this: any) { | ||
153 | const player = this | ||
154 | |||
155 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
156 | }) | ||
157 | } | ||
158 | |||
159 | private static loadLocaleInVideoJS (serverUrl: string, locale: string) { | ||
160 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
161 | // It is the default locale, nothing to translate | ||
162 | if (!path) return Promise.resolve(undefined) | ||
163 | |||
164 | let p: Promise<any> | ||
165 | |||
166 | if (PeertubePlayerManager.videojsLocaleCache[path]) { | ||
167 | p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path]) | ||
168 | } else { | ||
169 | p = fetch(path + '/player.json') | ||
170 | .then(res => res.json()) | ||
171 | .then(json => { | ||
172 | PeertubePlayerManager.videojsLocaleCache[path] = json | ||
173 | return json | ||
174 | }) | ||
175 | .catch(err => { | ||
176 | console.error('Cannot get player translations', err) | ||
177 | return undefined | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | const completeLocale = getCompleteLocale(locale) | ||
182 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
183 | } | ||
184 | |||
185 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { | ||
186 | const commonOptions = options.common | ||
187 | const webtorrentOptions = options.webtorrent | ||
188 | const p2pMediaLoaderOptions = options.p2pMediaLoader | ||
189 | |||
190 | let autoplay = options.common.autoplay | ||
191 | let html5 = {} | ||
192 | |||
193 | const plugins: VideoJSPluginOptions = { | ||
194 | peertube: { | ||
195 | mode, | ||
196 | autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
197 | videoViewUrl: commonOptions.videoViewUrl, | ||
198 | videoDuration: commonOptions.videoDuration, | ||
199 | startTime: commonOptions.startTime, | ||
200 | userWatching: commonOptions.userWatching, | ||
201 | subtitle: commonOptions.subtitle, | ||
202 | videoCaptions: commonOptions.videoCaptions | ||
203 | } | ||
204 | } | ||
205 | |||
206 | if (mode === 'p2p-media-loader') { | ||
207 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | ||
208 | redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls, | ||
209 | type: 'application/x-mpegURL', | ||
210 | src: p2pMediaLoaderOptions.playlistUrl | ||
211 | } | ||
212 | |||
213 | const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce | ||
214 | .filter(t => t.startsWith('ws')) | ||
215 | |||
216 | const p2pMediaLoaderConfig = { | ||
217 | loader: { | ||
218 | trackerAnnounce, | ||
219 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url), | ||
220 | rtcConfig: getRtcConfig(), | ||
221 | requiredSegmentsPriority: 5, | ||
222 | segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls) | ||
223 | }, | ||
224 | segments: { | ||
225 | swarmId: p2pMediaLoaderOptions.playlistUrl | ||
226 | } | ||
227 | } | ||
228 | const streamrootHls = { | ||
229 | levelLabelHandler: (level: { height: number, width: number }) => { | ||
230 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) | ||
231 | |||
232 | let label = file.resolution.label | ||
233 | if (file.fps >= 50) label += file.fps | ||
234 | |||
235 | return label | ||
236 | }, | ||
237 | html5: { | ||
238 | hlsjsConfig: { | ||
239 | liveSyncDurationCount: 7, | ||
240 | loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | |||
245 | Object.assign(plugins, { p2pMediaLoader, streamrootHls }) | ||
246 | html5 = streamrootHls.html5 | ||
247 | } | ||
248 | |||
249 | if (mode === 'webtorrent') { | ||
250 | const webtorrent = { | ||
251 | autoplay, | ||
252 | videoDuration: commonOptions.videoDuration, | ||
253 | playerElement: commonOptions.playerElement, | ||
254 | videoFiles: webtorrentOptions.videoFiles | ||
255 | } | ||
256 | Object.assign(plugins, { webtorrent }) | ||
257 | |||
258 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
259 | autoplay = false | ||
260 | } | ||
261 | |||
262 | const videojsOptions = { | ||
263 | html5, | ||
264 | |||
265 | // We don't use text track settings for now | ||
266 | textTrackSettings: false, | ||
267 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
268 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
269 | |||
270 | muted: commonOptions.muted !== undefined | ||
271 | ? commonOptions.muted | ||
272 | : undefined, // Undefined so the player knows it has to check the local storage | ||
273 | |||
274 | poster: commonOptions.poster, | ||
275 | autoplay: autoplay === true ? 'any' : autoplay, // Use 'any' instead of true to get notifier by videojs if autoplay fails | ||
276 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
277 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
278 | plugins, | ||
279 | controlBar: { | ||
280 | children: this.getControlBarChildren(mode, { | ||
281 | captions: commonOptions.captions, | ||
282 | peertubeLink: commonOptions.peertubeLink, | ||
283 | theaterMode: commonOptions.theaterMode | ||
284 | }) | ||
285 | } | ||
286 | } | ||
287 | |||
288 | if (commonOptions.enableHotkeys === true) { | ||
289 | Object.assign(videojsOptions.plugins, { | ||
290 | hotkeys: { | ||
291 | enableVolumeScroll: false, | ||
292 | enableModifiersForNumbers: false, | ||
293 | |||
294 | fullscreenKey: function (event: KeyboardEvent) { | ||
295 | // fullscreen with the f key or Ctrl+Enter | ||
296 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
297 | }, | ||
298 | |||
299 | seekStep: function (event: KeyboardEvent) { | ||
300 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
301 | if (event.ctrlKey && event.altKey) { | ||
302 | return 5 * 60 | ||
303 | } else if (event.ctrlKey) { | ||
304 | return 60 | ||
305 | } else if (event.altKey) { | ||
306 | return 10 | ||
307 | } else { | ||
308 | return 5 | ||
309 | } | ||
310 | }, | ||
311 | |||
312 | customKeys: { | ||
313 | increasePlaybackRateKey: { | ||
314 | key: function (event: KeyboardEvent) { | ||
315 | return event.key === '>' | ||
316 | }, | ||
317 | handler: function (player: videojs.Player) { | ||
318 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
319 | } | ||
320 | }, | ||
321 | decreasePlaybackRateKey: { | ||
322 | key: function (event: KeyboardEvent) { | ||
323 | return event.key === '<' | ||
324 | }, | ||
325 | handler: function (player: videojs.Player) { | ||
326 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
327 | } | ||
328 | }, | ||
329 | frameByFrame: { | ||
330 | key: function (event: KeyboardEvent) { | ||
331 | return event.key === '.' | ||
332 | }, | ||
333 | handler: function (player: videojs.Player) { | ||
334 | player.pause() | ||
335 | // Calculate movement distance (assuming 30 fps) | ||
336 | const dist = 1 / 30 | ||
337 | player.currentTime(player.currentTime() + dist) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | } | ||
342 | }) | ||
343 | } | ||
344 | |||
345 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
346 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
347 | } | ||
348 | |||
349 | return videojsOptions | ||
350 | } | ||
351 | |||
352 | private static getControlBarChildren (mode: PlayerMode, options: { | ||
353 | peertubeLink: boolean | ||
354 | theaterMode: boolean, | ||
355 | captions: boolean | ||
356 | }) { | ||
357 | const settingEntries = [] | ||
358 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' | ||
359 | |||
360 | // Keep an order | ||
361 | settingEntries.push('playbackRateMenuButton') | ||
362 | if (options.captions === true) settingEntries.push('captionsButton') | ||
363 | settingEntries.push('resolutionMenuButton') | ||
364 | |||
365 | const children = { | ||
366 | 'playToggle': {}, | ||
367 | 'currentTimeDisplay': {}, | ||
368 | 'timeDivider': {}, | ||
369 | 'durationDisplay': {}, | ||
370 | 'liveDisplay': {}, | ||
371 | |||
372 | 'flexibleWidthSpacer': {}, | ||
373 | 'progressControl': { | ||
374 | children: { | ||
375 | 'seekBar': { | ||
376 | children: { | ||
377 | [loadProgressBar]: {}, | ||
378 | 'mouseTimeDisplay': {}, | ||
379 | 'playProgressBar': {} | ||
380 | } | ||
381 | } | ||
382 | } | ||
383 | }, | ||
384 | |||
385 | 'p2PInfoButton': {}, | ||
386 | |||
387 | 'muteToggle': {}, | ||
388 | 'volumeControl': {}, | ||
389 | |||
390 | 'settingsButton': { | ||
391 | setup: { | ||
392 | maxHeightOffset: 40 | ||
393 | }, | ||
394 | entries: settingEntries | ||
395 | } | ||
396 | } | ||
397 | |||
398 | if (options.peertubeLink === true) { | ||
399 | Object.assign(children, { | ||
400 | 'peerTubeLinkButton': {} | ||
401 | }) | ||
402 | } | ||
403 | |||
404 | if (options.theaterMode === true) { | ||
405 | Object.assign(children, { | ||
406 | 'theaterButton': {} | ||
407 | }) | ||
408 | } | ||
409 | |||
410 | Object.assign(children, { | ||
411 | 'fullscreenToggle': {} | ||
412 | }) | ||
413 | |||
414 | return children | ||
415 | } | ||
416 | |||
417 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | ||
418 | const content = [ | ||
419 | { | ||
420 | label: player.localize('Copy the video URL'), | ||
421 | listener: function () { | ||
422 | copyToClipboard(buildVideoLink()) | ||
423 | } | ||
424 | }, | ||
425 | { | ||
426 | label: player.localize('Copy the video URL at the current time'), | ||
427 | listener: function () { | ||
428 | const player = this as videojs.Player | ||
429 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
430 | } | ||
431 | }, | ||
432 | { | ||
433 | label: player.localize('Copy embed code'), | ||
434 | listener: () => { | ||
435 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
436 | } | ||
437 | } | ||
438 | ] | ||
439 | |||
440 | if (mode === 'webtorrent') { | ||
441 | content.push({ | ||
442 | label: player.localize('Copy magnet URI'), | ||
443 | listener: function () { | ||
444 | const player = this as videojs.Player | ||
445 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
446 | } | ||
447 | }) | ||
448 | } | ||
449 | |||
450 | player.contextmenuUI({ content }) | ||
451 | } | ||
452 | |||
453 | private static getLocalePath (serverUrl: string, locale: string) { | ||
454 | const completeLocale = getCompleteLocale(locale) | ||
455 | |||
456 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
457 | |||
458 | return serverUrl + '/client/locales/' + completeLocale | ||
459 | } | ||
460 | } | ||
461 | |||
462 | // ############################################################################ | ||
463 | |||
464 | export { | ||
465 | videojs | ||
466 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts deleted file mode 100644 index 2de6d7fef..000000000 --- a/client/src/assets/player/peertube-player.ts +++ /dev/null | |||
@@ -1,300 +0,0 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | |||
3 | import 'videojs-hotkeys' | ||
4 | import 'videojs-dock' | ||
5 | import 'videojs-contextmenu-ui' | ||
6 | import './peertube-link-button' | ||
7 | import './resolution-menu-button' | ||
8 | import './settings-menu-button' | ||
9 | import './webtorrent-info-button' | ||
10 | import './peertube-videojs-plugin' | ||
11 | import './peertube-load-progress-bar' | ||
12 | import './theater-button' | ||
13 | import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' | ||
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
15 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
16 | |||
17 | // FIXME: something weird with our path definition in tsconfig and typings | ||
18 | // @ts-ignore | ||
19 | import { Player } from 'video.js' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
25 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | function getVideojsOptions (options: { | ||
29 | autoplay: boolean | ||
30 | playerElement: HTMLVideoElement | ||
31 | videoViewUrl: string | ||
32 | videoDuration: number | ||
33 | videoFiles: VideoFile[] | ||
34 | enableHotkeys: boolean | ||
35 | inactivityTimeout: number | ||
36 | peertubeLink: boolean | ||
37 | poster: string | ||
38 | startTime: number | string | ||
39 | theaterMode: boolean | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | |||
42 | language?: string | ||
43 | controls?: boolean | ||
44 | muted?: boolean | ||
45 | loop?: boolean | ||
46 | subtitle?: string | ||
47 | |||
48 | userWatching?: UserWatching | ||
49 | }) { | ||
50 | const videojsOptions = { | ||
51 | // We don't use text track settings for now | ||
52 | textTrackSettings: false, | ||
53 | controls: options.controls !== undefined ? options.controls : true, | ||
54 | loop: options.loop !== undefined ? options.loop : false, | ||
55 | |||
56 | muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage | ||
57 | |||
58 | poster: options.poster, | ||
59 | autoplay: false, | ||
60 | inactivityTimeout: options.inactivityTimeout, | ||
61 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
62 | plugins: { | ||
63 | peertube: { | ||
64 | autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
65 | videoCaptions: options.videoCaptions, | ||
66 | videoFiles: options.videoFiles, | ||
67 | playerElement: options.playerElement, | ||
68 | videoViewUrl: options.videoViewUrl, | ||
69 | videoDuration: options.videoDuration, | ||
70 | startTime: options.startTime, | ||
71 | userWatching: options.userWatching, | ||
72 | subtitle: options.subtitle | ||
73 | } | ||
74 | }, | ||
75 | controlBar: { | ||
76 | children: getControlBarChildren(options) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | if (options.enableHotkeys === true) { | ||
81 | Object.assign(videojsOptions.plugins, { | ||
82 | hotkeys: { | ||
83 | enableVolumeScroll: false, | ||
84 | enableModifiersForNumbers: false, | ||
85 | |||
86 | fullscreenKey: function (event: KeyboardEvent) { | ||
87 | // fullscreen with the f key or Ctrl+Enter | ||
88 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
89 | }, | ||
90 | |||
91 | seekStep: function (event: KeyboardEvent) { | ||
92 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
93 | if (event.ctrlKey && event.altKey) { | ||
94 | return 5 * 60 | ||
95 | } else if (event.ctrlKey) { | ||
96 | return 60 | ||
97 | } else if (event.altKey) { | ||
98 | return 10 | ||
99 | } else { | ||
100 | return 5 | ||
101 | } | ||
102 | }, | ||
103 | |||
104 | customKeys: { | ||
105 | increasePlaybackRateKey: { | ||
106 | key: function (event: KeyboardEvent) { | ||
107 | return event.key === '>' | ||
108 | }, | ||
109 | handler: function (player: Player) { | ||
110 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
111 | } | ||
112 | }, | ||
113 | decreasePlaybackRateKey: { | ||
114 | key: function (event: KeyboardEvent) { | ||
115 | return event.key === '<' | ||
116 | }, | ||
117 | handler: function (player: Player) { | ||
118 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
119 | } | ||
120 | }, | ||
121 | frameByFrame: { | ||
122 | key: function (event: KeyboardEvent) { | ||
123 | return event.key === '.' | ||
124 | }, | ||
125 | handler: function (player: Player) { | ||
126 | player.pause() | ||
127 | // Calculate movement distance (assuming 30 fps) | ||
128 | const dist = 1 / 30 | ||
129 | player.currentTime(player.currentTime() + dist) | ||
130 | } | ||
131 | } | ||
132 | } | ||
133 | } | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | if (options.language && !isDefaultLocale(options.language)) { | ||
138 | Object.assign(videojsOptions, { language: options.language }) | ||
139 | } | ||
140 | |||
141 | return videojsOptions | ||
142 | } | ||
143 | |||
144 | function getControlBarChildren (options: { | ||
145 | peertubeLink: boolean | ||
146 | theaterMode: boolean, | ||
147 | videoCaptions: VideoJSCaption[] | ||
148 | }) { | ||
149 | const settingEntries = [] | ||
150 | |||
151 | // Keep an order | ||
152 | settingEntries.push('playbackRateMenuButton') | ||
153 | if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton') | ||
154 | settingEntries.push('resolutionMenuButton') | ||
155 | |||
156 | const children = { | ||
157 | 'playToggle': {}, | ||
158 | 'currentTimeDisplay': {}, | ||
159 | 'timeDivider': {}, | ||
160 | 'durationDisplay': {}, | ||
161 | 'liveDisplay': {}, | ||
162 | |||
163 | 'flexibleWidthSpacer': {}, | ||
164 | 'progressControl': { | ||
165 | children: { | ||
166 | 'seekBar': { | ||
167 | children: { | ||
168 | 'peerTubeLoadProgressBar': {}, | ||
169 | 'mouseTimeDisplay': {}, | ||
170 | 'playProgressBar': {} | ||
171 | } | ||
172 | } | ||
173 | } | ||
174 | }, | ||
175 | |||
176 | 'webTorrentButton': {}, | ||
177 | |||
178 | 'muteToggle': {}, | ||
179 | 'volumeControl': {}, | ||
180 | |||
181 | 'settingsButton': { | ||
182 | setup: { | ||
183 | maxHeightOffset: 40 | ||
184 | }, | ||
185 | entries: settingEntries | ||
186 | } | ||
187 | } | ||
188 | |||
189 | if (options.peertubeLink === true) { | ||
190 | Object.assign(children, { | ||
191 | 'peerTubeLinkButton': {} | ||
192 | }) | ||
193 | } | ||
194 | |||
195 | if (options.theaterMode === true) { | ||
196 | Object.assign(children, { | ||
197 | 'theaterButton': {} | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | Object.assign(children, { | ||
202 | 'fullscreenToggle': {} | ||
203 | }) | ||
204 | |||
205 | return children | ||
206 | } | ||
207 | |||
208 | function addContextMenu (player: any, videoEmbedUrl: string) { | ||
209 | player.contextmenuUI({ | ||
210 | content: [ | ||
211 | { | ||
212 | label: player.localize('Copy the video URL'), | ||
213 | listener: function () { | ||
214 | copyToClipboard(buildVideoLink()) | ||
215 | } | ||
216 | }, | ||
217 | { | ||
218 | label: player.localize('Copy the video URL at the current time'), | ||
219 | listener: function () { | ||
220 | const player = this as Player | ||
221 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
222 | } | ||
223 | }, | ||
224 | { | ||
225 | label: player.localize('Copy embed code'), | ||
226 | listener: () => { | ||
227 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
228 | } | ||
229 | }, | ||
230 | { | ||
231 | label: player.localize('Copy magnet URI'), | ||
232 | listener: function () { | ||
233 | const player = this as Player | ||
234 | copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri) | ||
235 | } | ||
236 | } | ||
237 | ] | ||
238 | }) | ||
239 | } | ||
240 | |||
241 | function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) { | ||
242 | const path = getLocalePath(serverUrl, locale) | ||
243 | // It is the default locale, nothing to translate | ||
244 | if (!path) return Promise.resolve(undefined) | ||
245 | |||
246 | let p: Promise<any> | ||
247 | |||
248 | if (loadLocaleInVideoJS.cache[path]) { | ||
249 | p = Promise.resolve(loadLocaleInVideoJS.cache[path]) | ||
250 | } else { | ||
251 | p = fetch(path + '/player.json') | ||
252 | .then(res => res.json()) | ||
253 | .then(json => { | ||
254 | loadLocaleInVideoJS.cache[path] = json | ||
255 | return json | ||
256 | }) | ||
257 | .catch(err => { | ||
258 | console.error('Cannot get player translations', err) | ||
259 | return undefined | ||
260 | }) | ||
261 | } | ||
262 | |||
263 | const completeLocale = getCompleteLocale(locale) | ||
264 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
265 | } | ||
266 | namespace loadLocaleInVideoJS { | ||
267 | export const cache: { [ path: string ]: any } = {} | ||
268 | } | ||
269 | |||
270 | function getServerTranslations (serverUrl: string, locale: string) { | ||
271 | const path = getLocalePath(serverUrl, locale) | ||
272 | // It is the default locale, nothing to translate | ||
273 | if (!path) return Promise.resolve(undefined) | ||
274 | |||
275 | return fetch(path + '/server.json') | ||
276 | .then(res => res.json()) | ||
277 | .catch(err => { | ||
278 | console.error('Cannot get server translations', err) | ||
279 | return undefined | ||
280 | }) | ||
281 | } | ||
282 | |||
283 | // ############################################################################ | ||
284 | |||
285 | export { | ||
286 | getServerTranslations, | ||
287 | loadLocaleInVideoJS, | ||
288 | getVideojsOptions, | ||
289 | addContextMenu | ||
290 | } | ||
291 | |||
292 | // ############################################################################ | ||
293 | |||
294 | function getLocalePath (serverUrl: string, locale: string) { | ||
295 | const completeLocale = getCompleteLocale(locale) | ||
296 | |||
297 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
298 | |||
299 | return serverUrl + '/client/locales/' + completeLocale | ||
300 | } | ||
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts new file mode 100644 index 000000000..7ea4a06d4 --- /dev/null +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -0,0 +1,262 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | ||
5 | import { | ||
6 | PeerTubePluginOptions, | ||
7 | ResolutionUpdateData, | ||
8 | UserWatching, | ||
9 | VideoJSCaption, | ||
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | ||
13 | import { isMobile, timeToInt } from './utils' | ||
14 | import { | ||
15 | getStoredLastSubtitle, | ||
16 | getStoredMute, | ||
17 | getStoredVolume, | ||
18 | saveLastSubtitle, | ||
19 | saveMuteInStore, | ||
20 | saveVolumeInStore | ||
21 | } from './peertube-player-local-storage' | ||
22 | |||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
24 | class PeerTubePlugin extends Plugin { | ||
25 | private readonly autoplay: boolean = false | ||
26 | private readonly startTime: number = 0 | ||
27 | private readonly videoViewUrl: string | ||
28 | private readonly videoDuration: number | ||
29 | private readonly CONSTANTS = { | ||
30 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
31 | } | ||
32 | |||
33 | private player: any | ||
34 | private videoCaptions: VideoJSCaption[] | ||
35 | private defaultSubtitle: string | ||
36 | |||
37 | private videoViewInterval: any | ||
38 | private userWatchingVideoInterval: any | ||
39 | private qualityObservationTimer: any | ||
40 | private lastResolutionChange: ResolutionUpdateData | ||
41 | |||
42 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | ||
43 | super(player, options) | ||
44 | |||
45 | this.startTime = timeToInt(options.startTime) | ||
46 | this.videoViewUrl = options.videoViewUrl | ||
47 | this.videoDuration = options.videoDuration | ||
48 | this.videoCaptions = options.videoCaptions | ||
49 | |||
50 | if (options.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
51 | |||
52 | this.player.on('autoplay-failure', () => { | ||
53 | this.player.removeClass('vjs-has-autoplay') | ||
54 | }) | ||
55 | |||
56 | this.player.ready(() => { | ||
57 | const playerOptions = this.player.options_ | ||
58 | |||
59 | if (options.mode === 'webtorrent') { | ||
60 | this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
61 | this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) | ||
62 | } | ||
63 | |||
64 | if (options.mode === 'p2p-media-loader') { | ||
65 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
66 | } | ||
67 | |||
68 | this.player.tech_.on('loadedqualitydata', () => { | ||
69 | setTimeout(() => { | ||
70 | // Replay a resolution change, now we loaded all quality data | ||
71 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) | ||
72 | }, 0) | ||
73 | }) | ||
74 | |||
75 | const volume = getStoredVolume() | ||
76 | if (volume !== undefined) this.player.volume(volume) | ||
77 | |||
78 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
79 | if (muted !== undefined) this.player.muted(muted) | ||
80 | |||
81 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
82 | |||
83 | this.player.on('volumechange', () => { | ||
84 | saveVolumeInStore(this.player.volume()) | ||
85 | saveMuteInStore(this.player.muted()) | ||
86 | }) | ||
87 | |||
88 | this.player.textTracks().on('change', () => { | ||
89 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
90 | return t.kind === 'captions' && t.mode === 'showing' | ||
91 | }) | ||
92 | |||
93 | if (!showing) { | ||
94 | saveLastSubtitle('off') | ||
95 | return | ||
96 | } | ||
97 | |||
98 | saveLastSubtitle(showing.language) | ||
99 | }) | ||
100 | |||
101 | this.player.on('sourcechange', () => this.initCaptions()) | ||
102 | |||
103 | this.player.duration(options.videoDuration) | ||
104 | |||
105 | this.initializePlayer() | ||
106 | this.runViewAdd() | ||
107 | |||
108 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | dispose () { | ||
113 | clearTimeout(this.qualityObservationTimer) | ||
114 | |||
115 | clearInterval(this.videoViewInterval) | ||
116 | |||
117 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
118 | } | ||
119 | |||
120 | private initializePlayer () { | ||
121 | if (isMobile()) this.player.addClass('vjs-is-mobile') | ||
122 | |||
123 | this.initSmoothProgressBar() | ||
124 | |||
125 | this.initCaptions() | ||
126 | |||
127 | this.alterInactivity() | ||
128 | } | ||
129 | |||
130 | private runViewAdd () { | ||
131 | this.clearVideoViewInterval() | ||
132 | |||
133 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
134 | let minSecondsToView = 30 | ||
135 | |||
136 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
137 | |||
138 | let secondsViewed = 0 | ||
139 | this.videoViewInterval = setInterval(() => { | ||
140 | if (this.player && !this.player.paused()) { | ||
141 | secondsViewed += 1 | ||
142 | |||
143 | if (secondsViewed > minSecondsToView) { | ||
144 | this.clearVideoViewInterval() | ||
145 | |||
146 | this.addViewToVideo().catch(err => console.error(err)) | ||
147 | } | ||
148 | } | ||
149 | }, 1000) | ||
150 | } | ||
151 | |||
152 | private runUserWatchVideo (options: UserWatching) { | ||
153 | let lastCurrentTime = 0 | ||
154 | |||
155 | this.userWatchingVideoInterval = setInterval(() => { | ||
156 | const currentTime = Math.floor(this.player.currentTime()) | ||
157 | |||
158 | if (currentTime - lastCurrentTime >= 1) { | ||
159 | lastCurrentTime = currentTime | ||
160 | |||
161 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
162 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
163 | } | ||
164 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
165 | } | ||
166 | |||
167 | private clearVideoViewInterval () { | ||
168 | if (this.videoViewInterval !== undefined) { | ||
169 | clearInterval(this.videoViewInterval) | ||
170 | this.videoViewInterval = undefined | ||
171 | } | ||
172 | } | ||
173 | |||
174 | private addViewToVideo () { | ||
175 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
176 | |||
177 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
178 | } | ||
179 | |||
180 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
181 | const body = new URLSearchParams() | ||
182 | body.append('currentTime', currentTime.toString()) | ||
183 | |||
184 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
185 | |||
186 | return fetch(url, { method: 'PUT', body, headers }) | ||
187 | } | ||
188 | |||
189 | private handleResolutionChange (data: ResolutionUpdateData) { | ||
190 | this.lastResolutionChange = data | ||
191 | |||
192 | const qualityLevels = this.player.qualityLevels() | ||
193 | |||
194 | for (let i = 0; i < qualityLevels.length; i++) { | ||
195 | if (qualityLevels[i].height === data.resolutionId) { | ||
196 | data.id = qualityLevels[i].id | ||
197 | break | ||
198 | } | ||
199 | } | ||
200 | |||
201 | this.trigger('resolutionChange', data) | ||
202 | } | ||
203 | |||
204 | private alterInactivity () { | ||
205 | let saveInactivityTimeout: number | ||
206 | |||
207 | const disableInactivity = () => { | ||
208 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
209 | this.player.options_.inactivityTimeout = 0 | ||
210 | } | ||
211 | const enableInactivity = () => { | ||
212 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
213 | } | ||
214 | |||
215 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
216 | |||
217 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
218 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
219 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
220 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
221 | } | ||
222 | |||
223 | private initCaptions () { | ||
224 | for (const caption of this.videoCaptions) { | ||
225 | this.player.addRemoteTextTrack({ | ||
226 | kind: 'captions', | ||
227 | label: caption.label, | ||
228 | language: caption.language, | ||
229 | id: caption.language, | ||
230 | src: caption.src, | ||
231 | default: this.defaultSubtitle === caption.language | ||
232 | }, false) | ||
233 | } | ||
234 | |||
235 | this.player.trigger('captionsChanged') | ||
236 | } | ||
237 | |||
238 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | ||
239 | private initSmoothProgressBar () { | ||
240 | const SeekBar = videojsUntyped.getComponent('SeekBar') | ||
241 | SeekBar.prototype.getPercent = function getPercent () { | ||
242 | // Allows for smooth scrubbing, when player can't keep up. | ||
243 | // const time = (this.player_.scrubbing()) ? | ||
244 | // this.player_.getCache().currentTime : | ||
245 | // this.player_.currentTime() | ||
246 | const time = this.player_.currentTime() | ||
247 | const percent = time / this.player_.duration() | ||
248 | return percent >= 1 ? 1 : percent | ||
249 | } | ||
250 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | ||
251 | let newTime = this.calculateDistance(event) * this.player_.duration() | ||
252 | if (newTime === this.player_.duration()) { | ||
253 | newTime = newTime - 0.1 | ||
254 | } | ||
255 | this.player_.currentTime(newTime) | ||
256 | this.update() | ||
257 | } | ||
258 | } | ||
259 | } | ||
260 | |||
261 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
262 | export { PeerTubePlugin } | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 634c7fdc9..79a5a6c4d 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -3,11 +3,16 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
6 | import { PeerTubePlugin } from './peertube-videojs-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | ||
9 | import { PlayerMode } from './peertube-player-manager' | ||
7 | 10 | ||
8 | declare namespace videojs { | 11 | declare namespace videojs { |
9 | interface Player { | 12 | interface Player { |
10 | peertube (): PeerTubePlugin | 13 | peertube (): PeerTubePlugin |
14 | webtorrent (): WebTorrentPlugin | ||
15 | p2pMediaLoader (): P2pMediaLoaderPlugin | ||
11 | } | 16 | } |
12 | } | 17 | } |
13 | 18 | ||
@@ -30,26 +35,95 @@ type UserWatching = { | |||
30 | authorizationHeader: string | 35 | authorizationHeader: string |
31 | } | 36 | } |
32 | 37 | ||
33 | type PeertubePluginOptions = { | 38 | type PeerTubePluginOptions = { |
34 | videoFiles: VideoFile[] | 39 | mode: PlayerMode |
35 | playerElement: HTMLVideoElement | 40 | |
41 | autoplay: boolean | ||
36 | videoViewUrl: string | 42 | videoViewUrl: string |
37 | videoDuration: number | 43 | videoDuration: number |
38 | startTime: number | string | 44 | startTime: number | string |
39 | autoplay: boolean, | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | 45 | ||
42 | subtitle?: string | ||
43 | userWatching?: UserWatching | 46 | userWatching?: UserWatching |
47 | subtitle?: string | ||
48 | |||
49 | videoCaptions: VideoJSCaption[] | ||
50 | } | ||
51 | |||
52 | type WebtorrentPluginOptions = { | ||
53 | playerElement: HTMLVideoElement | ||
54 | |||
55 | autoplay: boolean | ||
56 | videoDuration: number | ||
57 | |||
58 | videoFiles: VideoFile[] | ||
59 | } | ||
60 | |||
61 | type P2PMediaLoaderPluginOptions = { | ||
62 | redundancyBaseUrls: string[] | ||
63 | type: string | ||
64 | src: string | ||
65 | } | ||
66 | |||
67 | type VideoJSPluginOptions = { | ||
68 | peertube: PeerTubePluginOptions | ||
69 | |||
70 | webtorrent?: WebtorrentPluginOptions | ||
71 | |||
72 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | ||
44 | } | 73 | } |
45 | 74 | ||
46 | // videojs typings don't have some method we need | 75 | // videojs typings don't have some method we need |
47 | const videojsUntyped = videojs as any | 76 | const videojsUntyped = videojs as any |
48 | 77 | ||
78 | type LoadedQualityData = { | ||
79 | qualitySwitchCallback: Function, | ||
80 | qualityData: { | ||
81 | video: { | ||
82 | id: number | ||
83 | label: string | ||
84 | selected: boolean | ||
85 | }[] | ||
86 | } | ||
87 | } | ||
88 | |||
89 | type ResolutionUpdateData = { | ||
90 | auto: boolean, | ||
91 | resolutionId: number | ||
92 | id?: number | ||
93 | } | ||
94 | |||
95 | type AutoResolutionUpdateData = { | ||
96 | possible: boolean | ||
97 | } | ||
98 | |||
99 | type PlayerNetworkInfo = { | ||
100 | http: { | ||
101 | downloadSpeed: number | ||
102 | uploadSpeed: number | ||
103 | downloaded: number | ||
104 | uploaded: number | ||
105 | } | ||
106 | |||
107 | p2p: { | ||
108 | downloadSpeed: number | ||
109 | uploadSpeed: number | ||
110 | downloaded: number | ||
111 | uploaded: number | ||
112 | numPeers: number | ||
113 | } | ||
114 | } | ||
115 | |||
49 | export { | 116 | export { |
117 | PlayerNetworkInfo, | ||
118 | ResolutionUpdateData, | ||
119 | AutoResolutionUpdateData, | ||
50 | VideoJSComponentInterface, | 120 | VideoJSComponentInterface, |
51 | PeertubePluginOptions, | ||
52 | videojsUntyped, | 121 | videojsUntyped, |
53 | VideoJSCaption, | 122 | VideoJSCaption, |
54 | UserWatching | 123 | UserWatching, |
124 | PeerTubePluginOptions, | ||
125 | WebtorrentPluginOptions, | ||
126 | P2PMediaLoaderPluginOptions, | ||
127 | VideoJSPluginOptions, | ||
128 | LoadedQualityData | ||
55 | } | 129 | } |
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts deleted file mode 100644 index a3c1108ca..000000000 --- a/client/src/assets/player/resolution-menu-button.ts +++ /dev/null | |||
@@ -1,88 +0,0 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class ResolutionMenuButton extends MenuButton { | ||
11 | label: HTMLElement | ||
12 | |||
13 | constructor (player: Player, options: any) { | ||
14 | super(player, options) | ||
15 | this.player = player | ||
16 | |||
17 | player.peertube().on('videoFileUpdate', () => this.updateLabel()) | ||
18 | player.peertube().on('autoResolutionUpdate', () => this.updateLabel()) | ||
19 | } | ||
20 | |||
21 | createEl () { | ||
22 | const el = super.createEl() | ||
23 | |||
24 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | ||
25 | className: 'vjs-resolution-value', | ||
26 | innerHTML: this.buildLabelHTML() | ||
27 | }) | ||
28 | |||
29 | el.appendChild(this.labelEl_) | ||
30 | |||
31 | return el | ||
32 | } | ||
33 | |||
34 | updateARIAAttributes () { | ||
35 | this.el().setAttribute('aria-label', 'Quality') | ||
36 | } | ||
37 | |||
38 | createMenu () { | ||
39 | const menu = new Menu(this.player_) | ||
40 | for (const videoFile of this.player_.peertube().videoFiles) { | ||
41 | let label = videoFile.resolution.label | ||
42 | if (videoFile.fps && videoFile.fps >= 50) { | ||
43 | label += videoFile.fps | ||
44 | } | ||
45 | |||
46 | menu.addChild(new ResolutionMenuItem( | ||
47 | this.player_, | ||
48 | { | ||
49 | id: videoFile.resolution.id, | ||
50 | label, | ||
51 | src: videoFile.magnetUri | ||
52 | }) | ||
53 | ) | ||
54 | } | ||
55 | |||
56 | menu.addChild(new ResolutionMenuItem( | ||
57 | this.player_, | ||
58 | { | ||
59 | id: -1, | ||
60 | label: this.player_.localize('Auto'), | ||
61 | src: null | ||
62 | } | ||
63 | )) | ||
64 | |||
65 | return menu | ||
66 | } | ||
67 | |||
68 | updateLabel () { | ||
69 | if (!this.labelEl_) return | ||
70 | |||
71 | this.labelEl_.innerHTML = this.buildLabelHTML() | ||
72 | } | ||
73 | |||
74 | buildCSSClass () { | ||
75 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
76 | } | ||
77 | |||
78 | buildWrapperCSSClass () { | ||
79 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
80 | } | ||
81 | |||
82 | private buildLabelHTML () { | ||
83 | return this.player_.peertube().getCurrentResolutionLabel() | ||
84 | } | ||
85 | } | ||
86 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
87 | |||
88 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | ||
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts deleted file mode 100644 index b54fd91ef..000000000 --- a/client/src/assets/player/resolution-menu-item.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | |||
10 | constructor (player: Player, options: any) { | ||
11 | const currentResolutionId = player.peertube().getCurrentResolutionId() | ||
12 | options.selectable = true | ||
13 | options.selected = options.id === currentResolutionId | ||
14 | |||
15 | super(player, options) | ||
16 | |||
17 | this.label = options.label | ||
18 | this.id = options.id | ||
19 | |||
20 | player.peertube().on('videoFileUpdate', () => this.updateSelection()) | ||
21 | player.peertube().on('autoResolutionUpdate', () => this.updateSelection()) | ||
22 | } | ||
23 | |||
24 | handleClick (event: any) { | ||
25 | if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return | ||
26 | |||
27 | super.handleClick(event) | ||
28 | |||
29 | // Auto resolution | ||
30 | if (this.id === -1) { | ||
31 | this.player_.peertube().enableAutoResolution() | ||
32 | return | ||
33 | } | ||
34 | |||
35 | this.player_.peertube().disableAutoResolution() | ||
36 | this.player_.peertube().updateResolution(this.id) | ||
37 | } | ||
38 | |||
39 | updateSelection () { | ||
40 | // Check if auto resolution is forbidden or not | ||
41 | if (this.id === -1) { | ||
42 | if (this.player_.peertube().isAutoResolutionForbidden()) { | ||
43 | this.addClass('disabled') | ||
44 | } else { | ||
45 | this.removeClass('disabled') | ||
46 | } | ||
47 | } | ||
48 | |||
49 | if (this.player_.peertube().isAutoResolutionOn()) { | ||
50 | this.selected(this.id === -1) | ||
51 | return | ||
52 | } | ||
53 | |||
54 | this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) | ||
55 | } | ||
56 | |||
57 | getLabel () { | ||
58 | if (this.id === -1) { | ||
59 | return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>' | ||
60 | } | ||
61 | |||
62 | return this.label | ||
63 | } | ||
64 | } | ||
65 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
66 | |||
67 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 8b9f34b99..8d87567c2 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -112,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) { | |||
112 | return min | 112 | return min |
113 | } | 113 | } |
114 | 114 | ||
115 | function getRtcConfig () { | ||
116 | return { | ||
117 | iceServers: [ | ||
118 | { | ||
119 | urls: 'stun:stun.stunprotocol.org' | ||
120 | }, | ||
121 | { | ||
122 | urls: 'stun:stun.framasoft.org' | ||
123 | } | ||
124 | ] | ||
125 | } | ||
126 | } | ||
127 | |||
115 | // --------------------------------------------------------------------------- | 128 | // --------------------------------------------------------------------------- |
116 | 129 | ||
117 | export { | 130 | export { |
131 | getRtcConfig, | ||
118 | toTitleCase, | 132 | toTitleCase, |
119 | timeToInt, | 133 | timeToInt, |
120 | buildVideoLink, | 134 | buildVideoLink, |
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index c3c1af951..6424787b2 100644 --- a/client/src/assets/player/webtorrent-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { bytes } from './utils' | 2 | import { bytes } from '../utils' |
3 | 3 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class WebtorrentInfoButton extends Button { | 5 | class P2pInfoButton extends Button { |
6 | 6 | ||
7 | createEl () { | 7 | createEl () { |
8 | const div = videojsUntyped.dom.createEl('div', { | 8 | const div = videojsUntyped.dom.createEl('div', { |
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button { | |||
65 | subDivHttp.appendChild(subDivHttpText) | 65 | subDivHttp.appendChild(subDivHttpText) |
66 | div.appendChild(subDivHttp) | 66 | div.appendChild(subDivHttp) |
67 | 67 | ||
68 | this.player_.peertube().on('torrentInfo', (event: any, data: any) => { | 68 | this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { |
69 | // We are in HTTP fallback | 69 | // We are in HTTP fallback |
70 | if (!data) { | 70 | if (!data) { |
71 | subDivHttp.className = 'vjs-peertube-displayed' | 71 | subDivHttp.className = 'vjs-peertube-displayed' |
@@ -74,11 +74,14 @@ class WebtorrentInfoButton extends Button { | |||
74 | return | 74 | return |
75 | } | 75 | } |
76 | 76 | ||
77 | const downloadSpeed = bytes(data.downloadSpeed) | 77 | const p2pStats = data.p2p |
78 | const uploadSpeed = bytes(data.uploadSpeed) | 78 | const httpStats = data.http |
79 | const totalDownloaded = bytes(data.downloaded) | 79 | |
80 | const totalUploaded = bytes(data.uploaded) | 80 | const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) |
81 | const numPeers = data.numPeers | 81 | const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) |
82 | const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) | ||
83 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) | ||
84 | const numPeers = p2pStats.numPeers | ||
82 | 85 | ||
83 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + | 86 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + |
84 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) | 87 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) |
@@ -90,7 +93,7 @@ class WebtorrentInfoButton extends Button { | |||
90 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | 93 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] |
91 | 94 | ||
92 | peersNumber.textContent = numPeers | 95 | peersNumber.textContent = numPeers |
93 | peersText.textContent = ' ' + this.player_.localize('peers') | 96 | peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) |
94 | 97 | ||
95 | subDivHttp.className = 'vjs-peertube-hidden' | 98 | subDivHttp.className = 'vjs-peertube-hidden' |
96 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 99 | subDivWebtorrent.className = 'vjs-peertube-displayed' |
@@ -99,4 +102,4 @@ class WebtorrentInfoButton extends Button { | |||
99 | return div | 102 | return div |
100 | } | 103 | } |
101 | } | 104 | } |
102 | Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) | 105 | Button.registerComponent('P2PInfoButton', P2pInfoButton) |
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index de9a49de9..fed8ea33e 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { buildVideoLink } from './utils' | 2 | import { buildVideoLink } from '../utils' |
3 | // FIXME: something weird with our path definition in tsconfig and typings | 3 | // FIXME: something weird with our path definition in tsconfig and typings |
4 | // @ts-ignore | 4 | // @ts-ignore |
5 | import { Player } from 'video.js' | 5 | import { Player } from 'video.js' |
diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index af276d1b2..9a0e3b550 100644 --- a/client/src/assets/player/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | 2 | // FIXME: something weird with our path definition in tsconfig and typings |
3 | // @ts-ignore | 3 | // @ts-ignore |
4 | import { Player } from 'video.js' | 4 | import { Player } from 'video.js' |
@@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component { | |||
27 | } | 27 | } |
28 | 28 | ||
29 | update () { | 29 | update () { |
30 | const torrent = this.player().peertube().getTorrent() | 30 | const torrent = this.player().webtorrent().getTorrent() |
31 | if (!torrent) return | 31 | if (!torrent) return |
32 | 32 | ||
33 | this.el_.style.width = (torrent.progress * 100) + '%' | 33 | this.el_.style.width = (torrent.progress * 100) + '%' |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts new file mode 100644 index 000000000..abcc16411 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -0,0 +1,109 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class ResolutionMenuButton extends MenuButton { | ||
11 | label: HTMLElement | ||
12 | |||
13 | constructor (player: Player, options: any) { | ||
14 | super(player, options) | ||
15 | this.player = player | ||
16 | |||
17 | player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | ||
18 | |||
19 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) | ||
20 | } | ||
21 | |||
22 | createEl () { | ||
23 | const el = super.createEl() | ||
24 | |||
25 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | ||
26 | className: 'vjs-resolution-value' | ||
27 | }) | ||
28 | |||
29 | el.appendChild(this.labelEl_) | ||
30 | |||
31 | return el | ||
32 | } | ||
33 | |||
34 | updateARIAAttributes () { | ||
35 | this.el().setAttribute('aria-label', 'Quality') | ||
36 | } | ||
37 | |||
38 | createMenu () { | ||
39 | return new Menu(this.player_) | ||
40 | } | ||
41 | |||
42 | buildCSSClass () { | ||
43 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
44 | } | ||
45 | |||
46 | buildWrapperCSSClass () { | ||
47 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
48 | } | ||
49 | |||
50 | private addClickListener (component: any) { | ||
51 | component.on('click', () => { | ||
52 | let children = this.menu.children() | ||
53 | |||
54 | for (const child of children) { | ||
55 | if (component !== child) { | ||
56 | child.selected(false) | ||
57 | } | ||
58 | } | ||
59 | }) | ||
60 | } | ||
61 | |||
62 | private buildQualities (data: LoadedQualityData) { | ||
63 | // The automatic resolution item will need other labels | ||
64 | const labels: { [ id: number ]: string } = {} | ||
65 | |||
66 | data.qualityData.video.sort((a, b) => { | ||
67 | if (a.id > b.id) return -1 | ||
68 | if (a.id === b.id) return 0 | ||
69 | return 1 | ||
70 | }) | ||
71 | |||
72 | for (const d of data.qualityData.video) { | ||
73 | // Skip auto resolution, we'll add it ourselves | ||
74 | if (d.id === -1) continue | ||
75 | |||
76 | this.menu.addChild(new ResolutionMenuItem( | ||
77 | this.player_, | ||
78 | { | ||
79 | id: d.id, | ||
80 | label: d.label, | ||
81 | selected: d.selected, | ||
82 | callback: data.qualitySwitchCallback | ||
83 | }) | ||
84 | ) | ||
85 | |||
86 | labels[d.id] = d.label | ||
87 | } | ||
88 | |||
89 | this.menu.addChild(new ResolutionMenuItem( | ||
90 | this.player_, | ||
91 | { | ||
92 | id: -1, | ||
93 | label: this.player_.localize('Auto'), | ||
94 | labels, | ||
95 | callback: data.qualitySwitchCallback, | ||
96 | selected: true // By default, in auto mode | ||
97 | } | ||
98 | )) | ||
99 | |||
100 | for (const m of this.menu.children()) { | ||
101 | this.addClickListener(m) | ||
102 | } | ||
103 | |||
104 | this.trigger('menuChanged') | ||
105 | } | ||
106 | } | ||
107 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
108 | |||
109 | MenuButton.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 new file mode 100644 index 000000000..6c42fefd2 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | private readonly id: number | ||
10 | private readonly label: string | ||
11 | // Only used for the automatic item | ||
12 | private readonly labels: { [id: number]: string } | ||
13 | private readonly callback: Function | ||
14 | |||
15 | private autoResolutionPossible: boolean | ||
16 | private currentResolutionLabel: string | ||
17 | |||
18 | constructor (player: Player, options: any) { | ||
19 | options.selectable = true | ||
20 | |||
21 | super(player, options) | ||
22 | |||
23 | this.autoResolutionPossible = true | ||
24 | this.currentResolutionLabel = '' | ||
25 | |||
26 | this.label = options.label | ||
27 | this.labels = options.labels | ||
28 | this.id = options.id | ||
29 | this.callback = options.callback | ||
30 | |||
31 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) | ||
32 | |||
33 | // We only want to disable the "Auto" item | ||
34 | if (this.id === -1) { | ||
35 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | handleClick (event: any) { | ||
40 | // Auto button disabled? | ||
41 | if (this.autoResolutionPossible === false && this.id === -1) return | ||
42 | |||
43 | super.handleClick(event) | ||
44 | |||
45 | this.callback(this.id, 'video') | ||
46 | } | ||
47 | |||
48 | updateSelection (data: ResolutionUpdateData) { | ||
49 | if (this.id === -1) { | ||
50 | this.currentResolutionLabel = this.labels[data.id] | ||
51 | } | ||
52 | |||
53 | // Automatic resolution only | ||
54 | if (data.auto === true) { | ||
55 | this.selected(this.id === -1) | ||
56 | return | ||
57 | } | ||
58 | |||
59 | this.selected(this.id === data.id) | ||
60 | } | ||
61 | |||
62 | updateAutoResolution (data: AutoResolutionUpdateData) { | ||
63 | // Check if the auto resolution is enabled or not | ||
64 | if (data.possible === false) { | ||
65 | this.addClass('disabled') | ||
66 | } else { | ||
67 | this.removeClass('disabled') | ||
68 | } | ||
69 | |||
70 | this.autoResolutionPossible = data.possible | ||
71 | } | ||
72 | |||
73 | getLabel () { | ||
74 | if (this.id === -1) { | ||
75 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' | ||
76 | } | ||
77 | |||
78 | return this.label | ||
79 | } | ||
80 | } | ||
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
82 | |||
83 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts index a7aefdcc3..14cb8ba43 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -6,8 +6,8 @@ | |||
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
11 | 11 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 2a3460ae5..f14959f9c 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -5,8 +5,8 @@ | |||
5 | // @ts-ignore | 5 | // @ts-ignore |
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { toTitleCase } from './utils' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | 10 | ||
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
@@ -220,12 +220,14 @@ class SettingsMenuItem extends MenuItem { | |||
220 | } | 220 | } |
221 | 221 | ||
222 | build () { | 222 | build () { |
223 | const saveUpdateLabel = this.subMenu.updateLabel | 223 | this.subMenu.on('updateLabel', () => { |
224 | this.subMenu.updateLabel = () => { | ||
225 | this.update() | 224 | this.update() |
226 | 225 | }) | |
227 | saveUpdateLabel.call(this.subMenu) | 226 | this.subMenu.on('menuChanged', () => { |
228 | } | 227 | this.bindClickEvents() |
228 | this.setSize() | ||
229 | this.update() | ||
230 | }) | ||
229 | 231 | ||
230 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 232 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
231 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 233 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) |
@@ -233,7 +235,7 @@ class SettingsMenuItem extends MenuItem { | |||
233 | this.update() | 235 | this.update() |
234 | 236 | ||
235 | this.createBackButton() | 237 | this.createBackButton() |
236 | this.getSize() | 238 | this.setSize() |
237 | this.bindClickEvents() | 239 | this.bindClickEvents() |
238 | 240 | ||
239 | // prefixed event listeners for CSS TransitionEnd | 241 | // prefixed event listeners for CSS TransitionEnd |
@@ -295,8 +297,9 @@ class SettingsMenuItem extends MenuItem { | |||
295 | 297 | ||
296 | // save size of submenus on first init | 298 | // save size of submenus on first init |
297 | // if number of submenu items change dynamically more logic will be needed | 299 | // if number of submenu items change dynamically more logic will be needed |
298 | getSize () { | 300 | setSize () { |
299 | this.dialog.removeClass('vjs-hidden') | 301 | this.dialog.removeClass('vjs-hidden') |
302 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
300 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | 303 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
301 | this.setMargin() | 304 | this.setMargin() |
302 | this.dialog.addClass('vjs-hidden') | 305 | this.dialog.addClass('vjs-hidden') |
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index 4f8fede3d..1e11a9546 100644 --- a/client/src/assets/player/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -2,8 +2,8 @@ | |||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
6 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 7 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
9 | class TheaterButton extends Button { | 9 | class TheaterButton extends Button { |
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts index 54cc0ea64..54cc0ea64 100644 --- a/client/src/assets/player/peertube-chunk-store.ts +++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts | |||
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts index a3415937b..a3415937b 100644 --- a/client/src/assets/player/video-renderer.ts +++ b/client/src/assets/player/webtorrent/video-renderer.ts | |||
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index e9fb90c61..c69bf31fa 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -3,23 +3,18 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
8 | import './settings-menu-button' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
9 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
10 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | ||
11 | import { PeertubeChunkStore } from './peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
12 | import { | 11 | import { |
13 | getAverageBandwidthInStore, | 12 | getAverageBandwidthInStore, |
14 | getStoredLastSubtitle, | ||
15 | getStoredMute, | 13 | getStoredMute, |
16 | getStoredVolume, | 14 | getStoredVolume, |
17 | getStoredWebTorrentEnabled, | 15 | getStoredWebTorrentEnabled, |
18 | saveAverageBandwidth, | 16 | saveAverageBandwidth |
19 | saveLastSubtitle, | 17 | } from '../peertube-player-local-storage' |
20 | saveMuteInStore, | ||
21 | saveVolumeInStore | ||
22 | } from './peertube-player-local-storage' | ||
23 | 18 | ||
24 | const CacheChunkStore = require('cache-chunk-store') | 19 | const CacheChunkStore = require('cache-chunk-store') |
25 | 20 | ||
@@ -30,14 +25,13 @@ type PlayOptions = { | |||
30 | } | 25 | } |
31 | 26 | ||
32 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
33 | class PeerTubePlugin extends Plugin { | 28 | class WebTorrentPlugin extends Plugin { |
34 | private readonly playerElement: HTMLVideoElement | 29 | private readonly playerElement: HTMLVideoElement |
35 | 30 | ||
36 | private readonly autoplay: boolean = false | 31 | private readonly autoplay: boolean = false |
37 | private readonly startTime: number = 0 | 32 | private readonly startTime: number = 0 |
38 | private readonly savePlayerSrcFunction: Function | 33 | private readonly savePlayerSrcFunction: Function |
39 | private readonly videoFiles: VideoFile[] | 34 | private readonly videoFiles: VideoFile[] |
40 | private readonly videoViewUrl: string | ||
41 | private readonly videoDuration: number | 35 | private readonly videoDuration: number |
42 | private readonly CONSTANTS = { | 36 | private readonly CONSTANTS = { |
43 | INFO_SCHEDULER: 1000, // Don't change this | 37 | INFO_SCHEDULER: 1000, // Don't change this |
@@ -45,22 +39,12 @@ class PeerTubePlugin extends Plugin { | |||
45 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it | 39 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it |
46 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check | 40 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check |
47 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds | 41 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds |
48 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth | 42 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth |
49 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
50 | } | 43 | } |
51 | 44 | ||
52 | private readonly webtorrent = new WebTorrent({ | 45 | private readonly webtorrent = new WebTorrent({ |
53 | tracker: { | 46 | tracker: { |
54 | rtcConfig: { | 47 | rtcConfig: getRtcConfig() |
55 | iceServers: [ | ||
56 | { | ||
57 | urls: 'stun:stun.stunprotocol.org' | ||
58 | }, | ||
59 | { | ||
60 | urls: 'stun:stun.framasoft.org' | ||
61 | } | ||
62 | ] | ||
63 | } | ||
64 | }, | 48 | }, |
65 | dht: false | 49 | dht: false |
66 | }) | 50 | }) |
@@ -68,46 +52,37 @@ class PeerTubePlugin extends Plugin { | |||
68 | private player: any | 52 | private player: any |
69 | private currentVideoFile: VideoFile | 53 | private currentVideoFile: VideoFile |
70 | private torrent: WebTorrent.Torrent | 54 | private torrent: WebTorrent.Torrent |
71 | private videoCaptions: VideoJSCaption[] | ||
72 | private defaultSubtitle: string | ||
73 | 55 | ||
74 | private renderer: any | 56 | private renderer: any |
75 | private fakeRenderer: any | 57 | private fakeRenderer: any |
76 | private destroyingFakeRenderer = false | 58 | private destroyingFakeRenderer = false |
77 | 59 | ||
78 | private autoResolution = true | 60 | private autoResolution = true |
79 | private forbidAutoResolution = false | 61 | private autoResolutionPossible = true |
80 | private isAutoResolutionObservation = false | 62 | private isAutoResolutionObservation = false |
81 | private playerRefusedP2P = false | 63 | private playerRefusedP2P = false |
82 | 64 | ||
83 | private videoViewInterval: any | ||
84 | private torrentInfoInterval: any | 65 | private torrentInfoInterval: any |
85 | private autoQualityInterval: any | 66 | private autoQualityInterval: any |
86 | private userWatchingVideoInterval: any | ||
87 | private addTorrentDelay: any | 67 | private addTorrentDelay: any |
88 | private qualityObservationTimer: any | 68 | private qualityObservationTimer: any |
89 | private runAutoQualitySchedulerTimer: any | 69 | private runAutoQualitySchedulerTimer: any |
90 | 70 | ||
91 | private downloadSpeeds: number[] = [] | 71 | private downloadSpeeds: number[] = [] |
92 | 72 | ||
93 | constructor (player: videojs.Player, options: PeertubePluginOptions) { | 73 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { |
94 | super(player, options) | 74 | super(player, options) |
95 | 75 | ||
96 | // Disable auto play on iOS | 76 | // Disable auto play on iOS |
97 | this.autoplay = options.autoplay && this.isIOS() === false | 77 | this.autoplay = options.autoplay && this.isIOS() === false |
98 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() | 78 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() |
99 | 79 | ||
100 | this.startTime = timeToInt(options.startTime) | ||
101 | this.videoFiles = options.videoFiles | 80 | this.videoFiles = options.videoFiles |
102 | this.videoViewUrl = options.videoViewUrl | ||
103 | this.videoDuration = options.videoDuration | 81 | this.videoDuration = options.videoDuration |
104 | this.videoCaptions = options.videoCaptions | ||
105 | 82 | ||
106 | this.savePlayerSrcFunction = this.player.src | 83 | this.savePlayerSrcFunction = this.player.src |
107 | this.playerElement = options.playerElement | 84 | this.playerElement = options.playerElement |
108 | 85 | ||
109 | if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
110 | |||
111 | this.player.ready(() => { | 86 | this.player.ready(() => { |
112 | const playerOptions = this.player.options_ | 87 | const playerOptions = this.player.options_ |
113 | 88 | ||
@@ -117,33 +92,10 @@ class PeerTubePlugin extends Plugin { | |||
117 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | 92 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() |
118 | if (muted !== undefined) this.player.muted(muted) | 93 | if (muted !== undefined) this.player.muted(muted) |
119 | 94 | ||
120 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
121 | |||
122 | this.player.on('volumechange', () => { | ||
123 | saveVolumeInStore(this.player.volume()) | ||
124 | saveMuteInStore(this.player.muted()) | ||
125 | }) | ||
126 | |||
127 | this.player.textTracks().on('change', () => { | ||
128 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
129 | return t.kind === 'captions' && t.mode === 'showing' | ||
130 | }) | ||
131 | |||
132 | if (!showing) { | ||
133 | saveLastSubtitle('off') | ||
134 | return | ||
135 | } | ||
136 | |||
137 | saveLastSubtitle(showing.language) | ||
138 | }) | ||
139 | |||
140 | this.player.duration(options.videoDuration) | 95 | this.player.duration(options.videoDuration) |
141 | 96 | ||
142 | this.initializePlayer() | 97 | this.initializePlayer() |
143 | this.runTorrentInfoScheduler() | 98 | this.runTorrentInfoScheduler() |
144 | this.runViewAdd() | ||
145 | |||
146 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
147 | 99 | ||
148 | this.player.one('play', () => { | 100 | this.player.one('play', () => { |
149 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | 101 | // Don't run immediately scheduler, wait some seconds the TCP connections are made |
@@ -157,12 +109,9 @@ class PeerTubePlugin extends Plugin { | |||
157 | clearTimeout(this.qualityObservationTimer) | 109 | clearTimeout(this.qualityObservationTimer) |
158 | clearTimeout(this.runAutoQualitySchedulerTimer) | 110 | clearTimeout(this.runAutoQualitySchedulerTimer) |
159 | 111 | ||
160 | clearInterval(this.videoViewInterval) | ||
161 | clearInterval(this.torrentInfoInterval) | 112 | clearInterval(this.torrentInfoInterval) |
162 | clearInterval(this.autoQualityInterval) | 113 | clearInterval(this.autoQualityInterval) |
163 | 114 | ||
164 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
165 | |||
166 | // Don't need to destroy renderer, video player will be destroyed | 115 | // Don't need to destroy renderer, video player will be destroyed |
167 | this.flushVideoFile(this.currentVideoFile, false) | 116 | this.flushVideoFile(this.currentVideoFile, false) |
168 | 117 | ||
@@ -173,13 +122,6 @@ class PeerTubePlugin extends Plugin { | |||
173 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | 122 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 |
174 | } | 123 | } |
175 | 124 | ||
176 | getCurrentResolutionLabel () { | ||
177 | if (!this.currentVideoFile) return '' | ||
178 | |||
179 | const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : '' | ||
180 | return this.currentVideoFile.resolution.label + fps | ||
181 | } | ||
182 | |||
183 | updateVideoFile ( | 125 | updateVideoFile ( |
184 | videoFile?: VideoFile, | 126 | videoFile?: VideoFile, |
185 | options: { | 127 | options: { |
@@ -228,7 +170,8 @@ class PeerTubePlugin extends Plugin { | |||
228 | return done() | 170 | return done() |
229 | }) | 171 | }) |
230 | 172 | ||
231 | this.trigger('videoFileUpdate') | 173 | this.changeQuality() |
174 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id }) | ||
232 | } | 175 | } |
233 | 176 | ||
234 | updateResolution (resolutionId: number, delay = 0) { | 177 | updateResolution (resolutionId: number, delay = 0) { |
@@ -262,28 +205,17 @@ class PeerTubePlugin extends Plugin { | |||
262 | } | 205 | } |
263 | } | 206 | } |
264 | 207 | ||
265 | isAutoResolutionOn () { | ||
266 | return this.autoResolution | ||
267 | } | ||
268 | |||
269 | enableAutoResolution () { | 208 | enableAutoResolution () { |
270 | this.autoResolution = true | 209 | this.autoResolution = true |
271 | this.trigger('autoResolutionUpdate') | 210 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
272 | } | 211 | } |
273 | 212 | ||
274 | disableAutoResolution (forbid = false) { | 213 | disableAutoResolution (forbid = false) { |
275 | if (forbid === true) this.forbidAutoResolution = true | 214 | if (forbid === true) this.autoResolutionPossible = false |
276 | 215 | ||
277 | this.autoResolution = false | 216 | this.autoResolution = false |
278 | this.trigger('autoResolutionUpdate') | 217 | this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible }) |
279 | } | 218 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
280 | |||
281 | isAutoResolutionForbidden () { | ||
282 | return this.forbidAutoResolution === true | ||
283 | } | ||
284 | |||
285 | getCurrentVideoFile () { | ||
286 | return this.currentVideoFile | ||
287 | } | 219 | } |
288 | 220 | ||
289 | getTorrent () { | 221 | getTorrent () { |
@@ -462,13 +394,7 @@ class PeerTubePlugin extends Plugin { | |||
462 | } | 394 | } |
463 | 395 | ||
464 | private initializePlayer () { | 396 | private initializePlayer () { |
465 | if (isMobile()) this.player.addClass('vjs-is-mobile') | 397 | this.buildQualities() |
466 | |||
467 | this.initSmoothProgressBar() | ||
468 | |||
469 | this.initCaptions() | ||
470 | |||
471 | this.alterInactivity() | ||
472 | 398 | ||
473 | if (this.autoplay === true) { | 399 | if (this.autoplay === true) { |
474 | this.player.posterImage.hide() | 400 | this.player.posterImage.hide() |
@@ -491,7 +417,7 @@ class PeerTubePlugin extends Plugin { | |||
491 | 417 | ||
492 | // Not initialized or in HTTP fallback | 418 | // Not initialized or in HTTP fallback |
493 | if (this.torrent === undefined || this.torrent === null) return | 419 | if (this.torrent === undefined || this.torrent === null) return |
494 | if (this.isAutoResolutionOn() === false) return | 420 | if (this.autoResolution === false) return |
495 | if (this.isAutoResolutionObservation === true) return | 421 | if (this.isAutoResolutionObservation === true) return |
496 | 422 | ||
497 | const file = this.getAppropriateFile() | 423 | const file = this.getAppropriateFile() |
@@ -531,78 +457,27 @@ class PeerTubePlugin extends Plugin { | |||
531 | if (this.torrent === undefined) return | 457 | if (this.torrent === undefined) return |
532 | 458 | ||
533 | // Http fallback | 459 | // Http fallback |
534 | if (this.torrent === null) return this.trigger('torrentInfo', false) | 460 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) |
535 | 461 | ||
536 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | 462 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too |
537 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | 463 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) |
538 | 464 | ||
539 | return this.trigger('torrentInfo', { | 465 | return this.player.trigger('p2pInfo', { |
540 | downloadSpeed: this.torrent.downloadSpeed, | 466 | http: { |
541 | numPeers: this.torrent.numPeers, | 467 | downloadSpeed: 0, |
542 | uploadSpeed: this.torrent.uploadSpeed, | 468 | uploadSpeed: 0, |
543 | downloaded: this.torrent.downloaded, | 469 | downloaded: 0, |
544 | uploaded: this.torrent.uploaded | 470 | uploaded: 0 |
545 | }) | 471 | }, |
546 | }, this.CONSTANTS.INFO_SCHEDULER) | 472 | p2p: { |
547 | } | 473 | downloadSpeed: this.torrent.downloadSpeed, |
548 | 474 | numPeers: this.torrent.numPeers, | |
549 | private runViewAdd () { | 475 | uploadSpeed: this.torrent.uploadSpeed, |
550 | this.clearVideoViewInterval() | 476 | downloaded: this.torrent.downloaded, |
551 | 477 | uploaded: this.torrent.uploaded | |
552 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
553 | let minSecondsToView = 30 | ||
554 | |||
555 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
556 | |||
557 | let secondsViewed = 0 | ||
558 | this.videoViewInterval = setInterval(() => { | ||
559 | if (this.player && !this.player.paused()) { | ||
560 | secondsViewed += 1 | ||
561 | |||
562 | if (secondsViewed > minSecondsToView) { | ||
563 | this.clearVideoViewInterval() | ||
564 | |||
565 | this.addViewToVideo().catch(err => console.error(err)) | ||
566 | } | 478 | } |
567 | } | 479 | } as PlayerNetworkInfo) |
568 | }, 1000) | 480 | }, this.CONSTANTS.INFO_SCHEDULER) |
569 | } | ||
570 | |||
571 | private runUserWatchVideo (options: UserWatching) { | ||
572 | let lastCurrentTime = 0 | ||
573 | |||
574 | this.userWatchingVideoInterval = setInterval(() => { | ||
575 | const currentTime = Math.floor(this.player.currentTime()) | ||
576 | |||
577 | if (currentTime - lastCurrentTime >= 1) { | ||
578 | lastCurrentTime = currentTime | ||
579 | |||
580 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
581 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
582 | } | ||
583 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
584 | } | ||
585 | |||
586 | private clearVideoViewInterval () { | ||
587 | if (this.videoViewInterval !== undefined) { | ||
588 | clearInterval(this.videoViewInterval) | ||
589 | this.videoViewInterval = undefined | ||
590 | } | ||
591 | } | ||
592 | |||
593 | private addViewToVideo () { | ||
594 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
595 | |||
596 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
597 | } | ||
598 | |||
599 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
600 | const body = new URLSearchParams() | ||
601 | body.append('currentTime', currentTime.toString()) | ||
602 | |||
603 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
604 | |||
605 | return fetch(url, { method: 'PUT', body, headers }) | ||
606 | } | 481 | } |
607 | 482 | ||
608 | private fallbackToHttp (options: PlayOptions, done?: Function) { | 483 | private fallbackToHttp (options: PlayOptions, done?: Function) { |
@@ -620,8 +495,10 @@ class PeerTubePlugin extends Plugin { | |||
620 | this.player.src = this.savePlayerSrcFunction | 495 | this.player.src = this.savePlayerSrcFunction |
621 | this.player.src(httpUrl) | 496 | this.player.src(httpUrl) |
622 | 497 | ||
498 | this.changeQuality() | ||
499 | |||
623 | // We changed the source, so reinit captions | 500 | // We changed the source, so reinit captions |
624 | this.initCaptions() | 501 | this.player.trigger('sourcechange') |
625 | 502 | ||
626 | return this.tryToPlay(err => { | 503 | return this.tryToPlay(err => { |
627 | if (err && done) return done(err) | 504 | if (err && done) return done(err) |
@@ -649,25 +526,6 @@ class PeerTubePlugin extends Plugin { | |||
649 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) | 526 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) |
650 | } | 527 | } |
651 | 528 | ||
652 | private alterInactivity () { | ||
653 | let saveInactivityTimeout: number | ||
654 | |||
655 | const disableInactivity = () => { | ||
656 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
657 | this.player.options_.inactivityTimeout = 0 | ||
658 | } | ||
659 | const enableInactivity = () => { | ||
660 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
661 | } | ||
662 | |||
663 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
664 | |||
665 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
666 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
667 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
668 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
669 | } | ||
670 | |||
671 | private pickAverageVideoFile () { | 529 | private pickAverageVideoFile () { |
672 | if (this.videoFiles.length === 1) return this.videoFiles[0] | 530 | if (this.videoFiles.length === 1) return this.videoFiles[0] |
673 | 531 | ||
@@ -712,43 +570,70 @@ class PeerTubePlugin extends Plugin { | |||
712 | } | 570 | } |
713 | } | 571 | } |
714 | 572 | ||
715 | private initCaptions () { | 573 | private buildQualities () { |
716 | for (const caption of this.videoCaptions) { | 574 | const qualityLevelsPayload = [] |
717 | this.player.addRemoteTextTrack({ | 575 | |
718 | kind: 'captions', | 576 | for (const file of this.videoFiles) { |
719 | label: caption.label, | 577 | const representation = { |
720 | language: caption.language, | 578 | id: file.resolution.id, |
721 | id: caption.language, | 579 | label: this.buildQualityLabel(file), |
722 | src: caption.src, | 580 | height: file.resolution.id, |
723 | default: this.defaultSubtitle === caption.language | 581 | _enabled: true |
724 | }, false) | 582 | } |
583 | |||
584 | this.player.qualityLevels().addQualityLevel(representation) | ||
585 | |||
586 | qualityLevelsPayload.push({ | ||
587 | id: representation.id, | ||
588 | label: representation.label, | ||
589 | selected: false | ||
590 | }) | ||
591 | } | ||
592 | |||
593 | const payload: LoadedQualityData = { | ||
594 | qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d), | ||
595 | qualityData: { | ||
596 | video: qualityLevelsPayload | ||
597 | } | ||
598 | } | ||
599 | this.player.tech_.trigger('loadedqualitydata', payload) | ||
600 | } | ||
601 | |||
602 | private buildQualityLabel (file: VideoFile) { | ||
603 | let label = file.resolution.label | ||
604 | |||
605 | if (file.fps && file.fps >= 50) { | ||
606 | label += file.fps | ||
725 | } | 607 | } |
726 | 608 | ||
727 | this.player.trigger('captionsChanged') | 609 | return label |
728 | } | 610 | } |
729 | 611 | ||
730 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 612 | private qualitySwitchCallback (id: number) { |
731 | private initSmoothProgressBar () { | 613 | if (id === -1) { |
732 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 614 | if (this.autoResolutionPossible === true) this.enableAutoResolution() |
733 | SeekBar.prototype.getPercent = function getPercent () { | 615 | return |
734 | // Allows for smooth scrubbing, when player can't keep up. | ||
735 | // const time = (this.player_.scrubbing()) ? | ||
736 | // this.player_.getCache().currentTime : | ||
737 | // this.player_.currentTime() | ||
738 | const time = this.player_.currentTime() | ||
739 | const percent = time / this.player_.duration() | ||
740 | return percent >= 1 ? 1 : percent | ||
741 | } | 616 | } |
742 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | 617 | |
743 | let newTime = this.calculateDistance(event) * this.player_.duration() | 618 | this.disableAutoResolution() |
744 | if (newTime === this.player_.duration()) { | 619 | this.updateResolution(id) |
745 | newTime = newTime - 0.1 | 620 | } |
746 | } | 621 | |
747 | this.player_.currentTime(newTime) | 622 | private changeQuality () { |
748 | this.update() | 623 | const resolutionId = this.currentVideoFile.resolution.id |
624 | const qualityLevels = this.player.qualityLevels() | ||
625 | |||
626 | if (resolutionId === -1) { | ||
627 | qualityLevels.selectedIndex = -1 | ||
628 | return | ||
629 | } | ||
630 | |||
631 | for (let i = 0; i < qualityLevels; i++) { | ||
632 | const q = this.player.qualityLevels[i] | ||
633 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | ||
749 | } | 634 | } |
750 | } | 635 | } |
751 | } | 636 | } |
752 | 637 | ||
753 | videojs.registerPlugin('peertube', PeerTubePlugin) | 638 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
754 | export { PeerTubePlugin } | 639 | export { WebTorrentPlugin } |
diff --git a/client/src/index.html b/client/src/index.html index 2af0020ad..8c257824e 100644 --- a/client/src/index.html +++ b/client/src/index.html | |||
@@ -5,7 +5,7 @@ | |||
5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | 6 | ||
7 | <meta name="theme-color" content="#fff" /> | 7 | <meta name="theme-color" content="#fff" /> |
8 | 8 | <meta property="og:platform" content="PeerTube" /> | |
9 | <!-- Web Manifest file --> | 9 | <!-- Web Manifest file --> |
10 | <link rel="manifest" href="/manifest.webmanifest"> | 10 | <link rel="manifest" href="/manifest.webmanifest"> |
11 | 11 | ||
diff --git a/client/src/main.ts b/client/src/main.ts index dee962180..86fdabba5 100644 --- a/client/src/main.ts +++ b/client/src/main.ts | |||
@@ -34,7 +34,7 @@ const bootstrap = () => platformBrowserDynamic() | |||
34 | // .catch(err => console.error('Cannot register service worker.', err)) | 34 | // .catch(err => console.error('Cannot register service worker.', err)) |
35 | // } | 35 | // } |
36 | 36 | ||
37 | if (navigator.serviceWorker) { | 37 | if (navigator.serviceWorker && typeof navigator.serviceWorker.getRegistrations === 'function') { |
38 | navigator.serviceWorker.getRegistrations() | 38 | navigator.serviceWorker.getRegistrations() |
39 | .then(registrations => { | 39 | .then(registrations => { |
40 | for (const registration of registrations) { | 40 | for (const registration of registrations) { |
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 2356f9837..478737a43 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss | |||
@@ -23,7 +23,7 @@ body { | |||
23 | // now beware node-sass requires interpolation | 23 | // now beware node-sass requires interpolation |
24 | // for css custom properties #{$var} | 24 | // for css custom properties #{$var} |
25 | --mainColor: #{$orange-color}; | 25 | --mainColor: #{$orange-color}; |
26 | --mainHoverColor: #{$orange-hoover-color}; | 26 | --mainHoverColor: #{$orange-hover-color}; |
27 | --mainBackgroundColor: #{$bg-color}; | 27 | --mainBackgroundColor: #{$bg-color}; |
28 | --mainForegroundColor: #{$fg-color}; | 28 | --mainForegroundColor: #{$fg-color}; |
29 | --menuBackgroundColor: #{$menu-background}; | 29 | --menuBackgroundColor: #{$menu-background}; |
@@ -229,13 +229,12 @@ label { | |||
229 | font-weight: $font-semibold; | 229 | font-weight: $font-semibold; |
230 | } | 230 | } |
231 | 231 | ||
232 | .close { | 232 | my-global-icon { |
233 | @include icon(24px); | 233 | @include icon(24px); |
234 | 234 | ||
235 | position: relative; | 235 | position: relative; |
236 | top: 3px; | 236 | top: 3px; |
237 | float: right; | 237 | float: right; |
238 | background-image: url('../assets/images/global/cross.svg'); | ||
239 | 238 | ||
240 | margin: 0; | 239 | margin: 0; |
241 | padding: 0; | 240 | padding: 0; |
@@ -293,6 +292,10 @@ ngb-tabset.bootstrap { | |||
293 | color: var(--mainForegroundColor) !important; | 292 | color: var(--mainForegroundColor) !important; |
294 | } | 293 | } |
295 | } | 294 | } |
295 | |||
296 | .nav-pills .nav-link.active { | ||
297 | color: #000 !important; | ||
298 | } | ||
296 | } | 299 | } |
297 | 300 | ||
298 | .nav-tabs .nav-link.active { | 301 | .nav-tabs .nav-link.active { |
@@ -324,7 +327,7 @@ ngb-tabset.bootstrap { | |||
324 | table { | 327 | table { |
325 | .action-button-edit, .action-button-delete { | 328 | .action-button-edit, .action-button-delete { |
326 | &:hover, &:active, &:focus, &[disabled], &.disabled { | 329 | &:hover, &:active, &:focus, &[disabled], &.disabled { |
327 | background-color: $grey-color !important; | 330 | background-color: $grey-background-color !important; |
328 | } | 331 | } |
329 | } | 332 | } |
330 | } | 333 | } |
@@ -389,4 +392,4 @@ table { | |||
389 | } | 392 | } |
390 | } | 393 | } |
391 | } | 394 | } |
392 | } \ No newline at end of file | 395 | } |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index d6f391a45..e18e9ae9d 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -55,6 +55,18 @@ | |||
55 | hyphens: auto; | 55 | hyphens: auto; |
56 | } | 56 | } |
57 | 57 | ||
58 | @mixin apply-svg-color ($color) { | ||
59 | /deep/ svg { | ||
60 | path[fill="#000000"], g[fill="#000000"], rect[fill="#000000"], circle[fill="#000000"] { | ||
61 | fill: $color; | ||
62 | } | ||
63 | |||
64 | path[stroke="#000000"], g[stroke="#000000"], rect[stroke="#000000"], circle[stroke="#000000"] { | ||
65 | stroke: $color; | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | |||
58 | @mixin peertube-input-text($width) { | 70 | @mixin peertube-input-text($width) { |
59 | display: inline-block; | 71 | display: inline-block; |
60 | height: $button-height; | 72 | height: $button-height; |
@@ -64,6 +76,7 @@ | |||
64 | border-radius: 3px; | 76 | border-radius: 3px; |
65 | padding-left: 15px; | 77 | padding-left: 15px; |
66 | padding-right: 15px; | 78 | padding-right: 15px; |
79 | font-size: 15px; | ||
67 | 80 | ||
68 | &::placeholder { | 81 | &::placeholder { |
69 | color: var(--inputPlaceholderColor); | 82 | color: var(--inputPlaceholderColor); |
@@ -110,22 +123,30 @@ | |||
110 | color: #fff; | 123 | color: #fff; |
111 | background-color: #C6C6C6; | 124 | background-color: #C6C6C6; |
112 | } | 125 | } |
126 | |||
127 | my-global-icon { | ||
128 | @include apply-svg-color(#fff) | ||
129 | } | ||
113 | } | 130 | } |
114 | 131 | ||
115 | @mixin grey-button { | 132 | @mixin grey-button { |
116 | &, &:active, &:focus { | 133 | &, &:active, &:focus { |
117 | background-color: $grey-color; | 134 | background-color: $grey-background-color; |
118 | color: #585858; | 135 | color: $grey-foreground-color; |
119 | } | 136 | } |
120 | 137 | ||
121 | &:hover, &:active, &:focus, &[disabled], &.disabled { | 138 | &:hover, &:active, &:focus, &[disabled], &.disabled { |
122 | color: #585858; | 139 | color: $grey-foreground-color; |
123 | background-color: $grey-hoover-color; | 140 | background-color: $grey-background-hover-color; |
124 | } | 141 | } |
125 | 142 | ||
126 | &[disabled], &.disabled { | 143 | &[disabled], &.disabled { |
127 | cursor: default; | 144 | cursor: default; |
128 | } | 145 | } |
146 | |||
147 | my-global-icon { | ||
148 | @include apply-svg-color($grey-foreground-color) | ||
149 | } | ||
129 | } | 150 | } |
130 | 151 | ||
131 | @mixin peertube-button { | 152 | @mixin peertube-button { |
@@ -148,6 +169,15 @@ | |||
148 | @include peertube-button; | 169 | @include peertube-button; |
149 | } | 170 | } |
150 | 171 | ||
172 | @mixin button-with-icon($width: 20px, $margin-right: 3px, $top: -1px) { | ||
173 | my-global-icon { | ||
174 | position: relative; | ||
175 | width: $width; | ||
176 | margin-right: $margin-right; | ||
177 | top: $top; | ||
178 | } | ||
179 | } | ||
180 | |||
151 | @mixin peertube-button-file ($width) { | 181 | @mixin peertube-button-file ($width) { |
152 | position: relative; | 182 | position: relative; |
153 | overflow: hidden; | 183 | overflow: hidden; |
@@ -231,6 +261,10 @@ | |||
231 | color: transparent; | 261 | color: transparent; |
232 | text-shadow: 0 0 0 #000; | 262 | text-shadow: 0 0 0 #000; |
233 | } | 263 | } |
264 | |||
265 | option { | ||
266 | color: #000; | ||
267 | } | ||
234 | } | 268 | } |
235 | } | 269 | } |
236 | 270 | ||
@@ -455,18 +489,10 @@ | |||
455 | } | 489 | } |
456 | } | 490 | } |
457 | 491 | ||
458 | @mixin create-button ($imageUrl) { | 492 | @mixin create-button { |
459 | @include peertube-button-link; | 493 | @include peertube-button-link; |
460 | @include orange-button; | 494 | @include orange-button; |
461 | 495 | @include button-with-icon(20px, 5px, -1px); | |
462 | .icon.icon-add { | ||
463 | @include icon(20px); | ||
464 | |||
465 | position: relative; | ||
466 | top: -1px; | ||
467 | margin-right: 5px; | ||
468 | background-image: url($imageUrl); | ||
469 | } | ||
470 | } | 496 | } |
471 | 497 | ||
472 | @mixin row-blocks { | 498 | @mixin row-blocks { |
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index fdf33b12a..3780b7501 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss | |||
@@ -6,10 +6,13 @@ $font-regular: 400; | |||
6 | $font-semibold: 600; | 6 | $font-semibold: 600; |
7 | $font-bold: 700; | 7 | $font-bold: 700; |
8 | 8 | ||
9 | $grey-color: #E5E5E5; | 9 | $grey-background-color: #E5E5E5; |
10 | $grey-hoover-color: #EFEFEF;; | 10 | $grey-background-hover-color: #EFEFEF; |
11 | $grey-foreground-color: #585858; | ||
12 | $grey-foreground-hover-color: #303030; | ||
13 | |||
11 | $orange-color: #F1680D; | 14 | $orange-color: #F1680D; |
12 | $orange-hoover-color: #F97D46; | 15 | $orange-hover-color: #F97D46; |
13 | 16 | ||
14 | $bg-color: #fff; | 17 | $bg-color: #fff; |
15 | $fg-color: #000; | 18 | $fg-color: #000; |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 6057b1db0..6e502b028 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -14,7 +14,7 @@ | |||
14 | p-table { | 14 | p-table { |
15 | .ui-table-caption { | 15 | .ui-table-caption { |
16 | border: none !important; | 16 | border: none !important; |
17 | background-color: #fff !important; | 17 | background-color: var(--mainBackgroundColor) !important; |
18 | 18 | ||
19 | .caption { | 19 | .caption { |
20 | height: 40px; | 20 | height: 40px; |
@@ -24,7 +24,7 @@ p-table { | |||
24 | } | 24 | } |
25 | 25 | ||
26 | th { | 26 | th { |
27 | background-color: #fff !important; | 27 | background-color: var(--mainBackgroundColor) !important; |
28 | outline: 0; | 28 | outline: 0; |
29 | } | 29 | } |
30 | 30 | ||
@@ -122,10 +122,14 @@ p-table { | |||
122 | 122 | ||
123 | &.pi-sort-up { | 123 | &.pi-sort-up { |
124 | @extend .glyphicon-triangle-top; | 124 | @extend .glyphicon-triangle-top; |
125 | |||
126 | color: var(--mainForegroundColor) !important; | ||
125 | } | 127 | } |
126 | 128 | ||
127 | &.pi-sort-down { | 129 | &.pi-sort-down { |
128 | @extend .glyphicon-triangle-bottom; | 130 | @extend .glyphicon-triangle-bottom; |
131 | |||
132 | color: var(--mainForegroundColor) !important; | ||
129 | } | 133 | } |
130 | } | 134 | } |
131 | } | 135 | } |
@@ -193,7 +197,7 @@ p-table { | |||
193 | height: auto !important; | 197 | height: auto !important; |
194 | 198 | ||
195 | a { | 199 | a { |
196 | color: #000 !important; | 200 | color: var(--mainForegroundColor) !important; |
197 | font-weight: $font-semibold !important; | 201 | font-weight: $font-semibold !important; |
198 | margin: 0 5px !important; | 202 | margin: 0 5px !important; |
199 | outline: 0 !important; | 203 | outline: 0 !important; |
@@ -230,6 +234,7 @@ p-calendar .ui-datepicker { | |||
230 | @extend .glyphicon-chevron-right; | 234 | @extend .glyphicon-chevron-right; |
231 | @include glyphicon-light; | 235 | @include glyphicon-light; |
232 | 236 | ||
237 | color: #000 !important; | ||
233 | text-align: right; | 238 | text-align: right; |
234 | 239 | ||
235 | .pi.pi-chevron-right { | 240 | .pi.pi-chevron-right { |
@@ -241,6 +246,7 @@ p-calendar .ui-datepicker { | |||
241 | @extend .glyphicon-chevron-left; | 246 | @extend .glyphicon-chevron-left; |
242 | @include glyphicon-light; | 247 | @include glyphicon-light; |
243 | 248 | ||
249 | color: #000 !important; | ||
244 | text-align: left; | 250 | text-align: left; |
245 | 251 | ||
246 | .pi.pi-chevron-left { | 252 | .pi.pi-chevron-left { |
@@ -254,42 +260,53 @@ p-calendar .ui-datepicker { | |||
254 | .pi.pi-chevron-up { | 260 | .pi.pi-chevron-up { |
255 | @extend .glyphicon-chevron-up; | 261 | @extend .glyphicon-chevron-up; |
256 | @include glyphicon-light; | 262 | @include glyphicon-light; |
263 | |||
264 | color: #000 !important; | ||
257 | } | 265 | } |
258 | 266 | ||
259 | .pi.pi-chevron-down { | 267 | .pi.pi-chevron-down { |
260 | @extend .glyphicon-chevron-down; | 268 | @extend .glyphicon-chevron-down; |
261 | @include glyphicon-light; | 269 | @include glyphicon-light; |
270 | |||
271 | color: #000 !important; | ||
262 | } | 272 | } |
263 | } | 273 | } |
264 | } | 274 | } |
265 | 275 | ||
276 | .ui-chkbox { | ||
266 | 277 | ||
267 | .ui-chkbox-box { | 278 | &, .ui-chkbox-box { |
268 | &.ui-state-active { | 279 | width: 18px !important; |
269 | border-color: var(--mainColor) !important; | 280 | height: 18px !important; |
270 | background-color: var(--mainColor) !important; | ||
271 | } | 281 | } |
272 | 282 | ||
273 | .ui-chkbox-icon { | 283 | .ui-chkbox-box { |
274 | position: relative; | 284 | &.ui-state-active { |
275 | overflow: visible !important; | 285 | border-color: var(--mainColor) !important; |
276 | 286 | background-color: var(--mainColor) !important; | |
277 | &:after { | ||
278 | content: ''; | ||
279 | position: absolute; | ||
280 | top: 1px; | ||
281 | left: 7px; | ||
282 | width: 5px; | ||
283 | height: 13px; | ||
284 | opacity: 0; | ||
285 | transform: rotate(45deg) scale(0); | ||
286 | border-right: 2px solid var(--mainBackgroundColor); | ||
287 | border-bottom: 2px solid var(--mainBackgroundColor); | ||
288 | } | 287 | } |
289 | 288 | ||
290 | &.pi-check:after { | 289 | .ui-chkbox-icon { |
291 | opacity: 1; | 290 | position: relative; |
292 | transform: rotate(45deg) scale(1); | 291 | overflow: visible !important; |
292 | |||
293 | &:after { | ||
294 | content: ''; | ||
295 | position: absolute; | ||
296 | top: 1px; | ||
297 | left: 6px; | ||
298 | width: 5px; | ||
299 | height: 12px; | ||
300 | opacity: 0; | ||
301 | transform: rotate(45deg) scale(0); | ||
302 | border-right: 2px solid var(--mainBackgroundColor); | ||
303 | border-bottom: 2px solid var(--mainBackgroundColor); | ||
304 | } | ||
305 | |||
306 | &.pi-check:after { | ||
307 | opacity: 1; | ||
308 | transform: rotate(45deg) scale(1); | ||
309 | } | ||
293 | } | 310 | } |
294 | } | 311 | } |
295 | } | 312 | } |
@@ -354,3 +371,7 @@ p-toast { | |||
354 | } | 371 | } |
355 | } | 372 | } |
356 | } | 373 | } |
374 | |||
375 | .ui-widget { | ||
376 | font-family: $main-fonts !important; | ||
377 | } | ||
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index f79cf68df..c3b6e08ca 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -6,6 +6,7 @@ | |||
6 | <meta charset="UTF-8"> | 6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 7 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
8 | <meta name="robots" content="noindex"> | 8 | <meta name="robots" content="noindex"> |
9 | <meta property="og:platform" content="PeerTube" /> | ||
9 | 10 | ||
10 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 11 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> |
11 | </head> | 12 | </head> |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 54b8fb543..32bf42e12 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -17,17 +17,19 @@ import 'core-js/es6/set' | |||
17 | // For google bot that uses Chrome 41 and does not understand fetch | 17 | // For google bot that uses Chrome 41 and does not understand fetch |
18 | import 'whatwg-fetch' | 18 | import 'whatwg-fetch' |
19 | 19 | ||
20 | // FIXME: something weird with our path definition in tsconfig and typings | ||
21 | // @ts-ignore | ||
22 | import * as vjs from 'video.js' | ||
23 | |||
24 | import * as Channel from 'jschannel' | 20 | import * as Channel from 'jschannel' |
25 | 21 | ||
26 | import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' | 22 | import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' |
27 | import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player' | ||
28 | import { PeerTubeResolution } from '../player/definitions' | 23 | import { PeerTubeResolution } from '../player/definitions' |
29 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' | 24 | import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' |
30 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 25 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' |
26 | import { | ||
27 | P2PMediaLoaderOptions, | ||
28 | PeertubePlayerManager, | ||
29 | PeertubePlayerManagerOptions, | ||
30 | PlayerMode | ||
31 | } from '../../assets/player/peertube-player-manager' | ||
32 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
31 | 33 | ||
32 | /** | 34 | /** |
33 | * Embed API exposes control of the embed player to the outside world via | 35 | * Embed API exposes control of the embed player to the outside world via |
@@ -73,16 +75,16 @@ class PeerTubeEmbedApi { | |||
73 | } | 75 | } |
74 | 76 | ||
75 | private setResolution (resolutionId: number) { | 77 | private setResolution (resolutionId: number) { |
76 | if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return | 78 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return |
77 | 79 | ||
78 | // Auto resolution | 80 | // Auto resolution |
79 | if (resolutionId === -1) { | 81 | if (resolutionId === -1) { |
80 | this.embed.player.peertube().enableAutoResolution() | 82 | this.embed.player.webtorrent().enableAutoResolution() |
81 | return | 83 | return |
82 | } | 84 | } |
83 | 85 | ||
84 | this.embed.player.peertube().disableAutoResolution() | 86 | this.embed.player.webtorrent().disableAutoResolution() |
85 | this.embed.player.peertube().updateResolution(resolutionId) | 87 | this.embed.player.webtorrent().updateResolution(resolutionId) |
86 | } | 88 | } |
87 | 89 | ||
88 | /** | 90 | /** |
@@ -122,15 +124,17 @@ class PeerTubeEmbedApi { | |||
122 | 124 | ||
123 | // PeerTube specific capabilities | 125 | // PeerTube specific capabilities |
124 | 126 | ||
125 | this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions()) | 127 | if (this.embed.player.webtorrent) { |
126 | this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions()) | 128 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) |
129 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
130 | } | ||
127 | } | 131 | } |
128 | 132 | ||
129 | private loadResolutions () { | 133 | private loadWebTorrentResolutions () { |
130 | let resolutions = [] | 134 | let resolutions = [] |
131 | let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId() | 135 | let currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() |
132 | 136 | ||
133 | for (const videoFile of this.embed.player.peertube().videoFiles) { | 137 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { |
134 | let label = videoFile.resolution.label | 138 | let label = videoFile.resolution.label |
135 | if (videoFile.fps && videoFile.fps >= 50) { | 139 | if (videoFile.fps && videoFile.fps >= 50) { |
136 | label += videoFile.fps | 140 | label += videoFile.fps |
@@ -164,6 +168,7 @@ class PeerTubeEmbed { | |||
164 | subtitle: string | 168 | subtitle: string |
165 | enableApi = false | 169 | enableApi = false |
166 | startTime: number | string = 0 | 170 | startTime: number | string = 0 |
171 | mode: PlayerMode | ||
167 | scope = 'peertube' | 172 | scope = 'peertube' |
168 | 173 | ||
169 | static async main () { | 174 | static async main () { |
@@ -257,6 +262,8 @@ class PeerTubeEmbed { | |||
257 | this.scope = this.getParamString(params, 'scope', this.scope) | 262 | this.scope = this.getParamString(params, 'scope', this.scope) |
258 | this.subtitle = this.getParamString(params, 'subtitle') | 263 | this.subtitle = this.getParamString(params, 'subtitle') |
259 | this.startTime = this.getParamString(params, 'start') | 264 | this.startTime = this.getParamString(params, 'start') |
265 | |||
266 | this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' | ||
260 | } catch (err) { | 267 | } catch (err) { |
261 | console.error('Cannot get params from URL.', err) | 268 | console.error('Cannot get params from URL.', err) |
262 | } | 269 | } |
@@ -266,9 +273,8 @@ class PeerTubeEmbed { | |||
266 | const urlParts = window.location.pathname.split('/') | 273 | const urlParts = window.location.pathname.split('/') |
267 | const videoId = urlParts[ urlParts.length - 1 ] | 274 | const videoId = urlParts[ urlParts.length - 1 ] |
268 | 275 | ||
269 | const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ | 276 | const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ |
270 | loadLocaleInVideoJS(window.location.origin, vjs, navigator.language), | 277 | PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language), |
271 | getServerTranslations(window.location.origin, navigator.language), | ||
272 | this.loadVideoInfo(videoId), | 278 | this.loadVideoInfo(videoId), |
273 | this.loadVideoCaptions(videoId) | 279 | this.loadVideoCaptions(videoId) |
274 | ]) | 280 | ]) |
@@ -292,43 +298,67 @@ class PeerTubeEmbed { | |||
292 | 298 | ||
293 | this.loadParams() | 299 | this.loadParams() |
294 | 300 | ||
295 | const videojsOptions = getVideojsOptions({ | 301 | const options: PeertubePlayerManagerOptions = { |
296 | autoplay: this.autoplay, | 302 | common: { |
297 | controls: this.controls, | 303 | autoplay: this.autoplay, |
298 | muted: this.muted, | 304 | controls: this.controls, |
299 | loop: this.loop, | 305 | muted: this.muted, |
300 | startTime: this.startTime, | 306 | loop: this.loop, |
301 | subtitle: this.subtitle, | 307 | captions: videoCaptions.length !== 0, |
302 | 308 | startTime: this.startTime, | |
303 | videoCaptions, | 309 | subtitle: this.subtitle, |
304 | inactivityTimeout: 1500, | 310 | |
305 | videoViewUrl: this.getVideoUrl(videoId) + '/views', | 311 | videoCaptions, |
306 | playerElement: this.videoElement, | 312 | inactivityTimeout: 1500, |
307 | videoFiles: videoInfo.files, | 313 | videoViewUrl: this.getVideoUrl(videoId) + '/views', |
308 | videoDuration: videoInfo.duration, | 314 | |
309 | enableHotkeys: true, | 315 | playerElement: this.videoElement, |
310 | peertubeLink: true, | 316 | onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element, |
311 | poster: window.location.origin + videoInfo.previewPath, | 317 | |
312 | theaterMode: false | 318 | videoDuration: videoInfo.duration, |
313 | }) | 319 | enableHotkeys: true, |
320 | peertubeLink: true, | ||
321 | poster: window.location.origin + videoInfo.previewPath, | ||
322 | theaterMode: false, | ||
323 | |||
324 | serverUrl: window.location.origin, | ||
325 | language: navigator.language, | ||
326 | embedUrl: window.location.origin + videoInfo.embedPath | ||
327 | }, | ||
328 | |||
329 | webtorrent: { | ||
330 | videoFiles: videoInfo.files | ||
331 | } | ||
332 | } | ||
314 | 333 | ||
315 | this.playerOptions = videojsOptions | 334 | if (this.mode === 'p2p-media-loader') { |
316 | this.player = vjs(this.videoContainerId, videojsOptions, () => { | 335 | const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) |
317 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) | 336 | |
337 | Object.assign(options, { | ||
338 | p2pMediaLoader: { | ||
339 | playlistUrl: hlsPlaylist.playlistUrl, | ||
340 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
341 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
342 | trackerAnnounce: videoInfo.trackerUrls, | ||
343 | videoFiles: videoInfo.files | ||
344 | } as P2PMediaLoaderOptions | ||
345 | }) | ||
346 | } | ||
318 | 347 | ||
319 | window[ 'videojsPlayer' ] = this.player | 348 | this.player = await PeertubePlayerManager.initialize(this.mode, options) |
320 | 349 | ||
321 | if (this.controls) { | 350 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) |
322 | this.player.dock({ | ||
323 | title: videoInfo.name, | ||
324 | description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
325 | }) | ||
326 | } | ||
327 | 351 | ||
328 | addContextMenu(this.player, window.location.origin + videoInfo.embedPath) | 352 | window[ 'videojsPlayer' ] = this.player |
329 | 353 | ||
330 | this.initializeApi() | 354 | if (this.controls) { |
331 | }) | 355 | this.player.dock({ |
356 | title: videoInfo.name, | ||
357 | description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') | ||
358 | }) | ||
359 | } | ||
360 | |||
361 | this.initializeApi() | ||
332 | } | 362 | } |
333 | 363 | ||
334 | private handleError (err: Error, translations?: { [ id: string ]: string }) { | 364 | private handleError (err: Error, translations?: { [ id: string ]: string }) { |
diff --git a/client/src/tsconfig.app.json b/client/src/tsconfig.app.json index af7a74e9e..729eee353 100644 --- a/client/src/tsconfig.app.json +++ b/client/src/tsconfig.app.json | |||
@@ -3,7 +3,7 @@ | |||
3 | "compilerOptions": { | 3 | "compilerOptions": { |
4 | "outDir": "../out-tsc/app", | 4 | "outDir": "../out-tsc/app", |
5 | "baseUrl": "./", | 5 | "baseUrl": "./", |
6 | "module": "es2015", | 6 | "module": "esnext", |
7 | "types": [], | 7 | "types": [], |
8 | "lib": [ | 8 | "lib": [ |
9 | "es2017", | 9 | "es2017", |
diff --git a/client/tsconfig.json b/client/tsconfig.json index beca79e01..3f9986f8a 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json | |||
@@ -5,6 +5,7 @@ | |||
5 | "sourceMap": true, | 5 | "sourceMap": true, |
6 | "declaration": false, | 6 | "declaration": false, |
7 | "moduleResolution": "node", | 7 | "moduleResolution": "node", |
8 | "module": "esnext", | ||
8 | "emitDecoratorMetadata": true, | 9 | "emitDecoratorMetadata": true, |
9 | "experimentalDecorators": true, | 10 | "experimentalDecorators": true, |
10 | "noImplicitAny": true, | 11 | "noImplicitAny": true, |
diff --git a/client/yarn.lock b/client/yarn.lock index 5ed43117a..63394e0f7 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -2,139 +2,141 @@ | |||
2 | # yarn lockfile v1 | 2 | # yarn lockfile v1 |
3 | 3 | ||
4 | 4 | ||
5 | "@angular-devkit/architect@0.11.1": | 5 | "@angular-devkit/architect@0.13.1": |
6 | version "0.11.1" | 6 | version "0.13.1" |
7 | resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.11.1.tgz#fb8429b583d4d7efafe5ff551ffdd30a705b9ab0" | 7 | resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.13.1.tgz#39597ce94f72d89bdd89ee567cb937cff4c13b98" |
8 | integrity sha512-MdcZ5KclwL2SBXCQSn8uI2hakBX58EyuAwFWsM/pKrNt9j8RqIk93l4amd2OkaMtZRFP5zWodyf/3qOwacjuQg== | 8 | integrity sha512-QDmIbqde75ZZSEFbw6Q6kQWq4cY6C7D67yujXw6XTyubDNAs1tyXJyxTIB8vjSlEKwRizTTDd/B0ZXVcke3Mvw== |
9 | dependencies: | 9 | dependencies: |
10 | "@angular-devkit/core" "7.1.1" | 10 | "@angular-devkit/core" "7.3.1" |
11 | rxjs "6.3.3" | 11 | rxjs "6.3.3" |
12 | 12 | ||
13 | "@angular-devkit/build-angular@~0.11.1": | 13 | "@angular-devkit/build-angular@~0.13.1": |
14 | version "0.11.1" | 14 | version "0.13.1" |
15 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.11.1.tgz#a828797d9177227aee70a65bb06d4b189b1404cd" | 15 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.13.1.tgz#369febda48dd40e47a4f0077064e792612a8e1c1" |
16 | integrity sha512-hA/3GVMmRwOPXWhImrBG9gZTdERr937NMuedKhTXuNj6TNMNjk9XQ+q2erd0LZVbgfhL/nC0wHnpy0dUWXu8jA== | 16 | integrity sha512-vkKwMVQ+NNCcVR3HFMffS+Mq4b2afXeUjI+02N38hBuFTppnC83uivUB6Uu2NUk5NTSQA4BnJlG5CbMs6N4QYg== |
17 | dependencies: | 17 | dependencies: |
18 | "@angular-devkit/architect" "0.11.1" | 18 | "@angular-devkit/architect" "0.13.1" |
19 | "@angular-devkit/build-optimizer" "0.11.1" | 19 | "@angular-devkit/build-optimizer" "0.13.1" |
20 | "@angular-devkit/build-webpack" "0.11.1" | 20 | "@angular-devkit/build-webpack" "0.13.1" |
21 | "@angular-devkit/core" "7.1.1" | 21 | "@angular-devkit/core" "7.3.1" |
22 | "@ngtools/webpack" "7.1.1" | 22 | "@ngtools/webpack" "7.3.1" |
23 | ajv "6.5.3" | 23 | ajv "6.7.0" |
24 | autoprefixer "9.3.1" | 24 | autoprefixer "9.4.6" |
25 | circular-dependency-plugin "5.0.2" | 25 | circular-dependency-plugin "5.0.2" |
26 | clean-css "4.2.1" | 26 | clean-css "4.2.1" |
27 | copy-webpack-plugin "4.5.4" | 27 | copy-webpack-plugin "4.6.0" |
28 | file-loader "2.0.0" | 28 | file-loader "3.0.1" |
29 | glob "7.1.3" | 29 | glob "7.1.3" |
30 | istanbul "0.4.5" | ||
31 | istanbul-instrumenter-loader "3.0.1" | 30 | istanbul-instrumenter-loader "3.0.1" |
32 | karma-source-map-support "1.3.0" | 31 | karma-source-map-support "1.3.0" |
33 | less "3.8.1" | 32 | less "3.9.0" |
34 | less-loader "4.1.0" | 33 | less-loader "4.1.0" |
35 | license-webpack-plugin "2.0.2" | 34 | license-webpack-plugin "2.1.0" |
36 | loader-utils "1.1.0" | 35 | loader-utils "1.2.3" |
37 | mini-css-extract-plugin "0.4.4" | 36 | mini-css-extract-plugin "0.5.0" |
38 | minimatch "3.0.4" | 37 | minimatch "3.0.4" |
39 | opn "5.3.0" | 38 | opn "5.4.0" |
40 | parse5 "4.0.0" | 39 | parse5 "4.0.0" |
41 | portfinder "1.0.17" | 40 | postcss "7.0.14" |
42 | postcss "7.0.5" | 41 | postcss-import "12.0.1" |
43 | postcss-import "12.0.0" | ||
44 | postcss-loader "3.0.0" | 42 | postcss-loader "3.0.0" |
45 | raw-loader "0.5.1" | 43 | raw-loader "1.0.0" |
46 | rxjs "6.3.3" | 44 | rxjs "6.3.3" |
47 | sass-loader "7.1.0" | 45 | sass-loader "7.1.0" |
48 | semver "5.5.1" | 46 | semver "5.6.0" |
49 | source-map-loader "0.2.4" | 47 | source-map-loader "0.2.4" |
50 | source-map-support "0.5.9" | 48 | source-map-support "0.5.10" |
51 | speed-measure-webpack-plugin "1.2.3" | 49 | speed-measure-webpack-plugin "1.3.0" |
52 | stats-webpack-plugin "0.7.0" | 50 | stats-webpack-plugin "0.7.0" |
53 | style-loader "0.23.1" | 51 | style-loader "0.23.1" |
54 | stylus "0.54.5" | 52 | stylus "0.54.5" |
55 | stylus-loader "3.0.2" | 53 | stylus-loader "3.0.2" |
56 | terser-webpack-plugin "1.1.0" | 54 | terser-webpack-plugin "1.2.2" |
57 | tree-kill "1.2.0" | 55 | tree-kill "1.2.1" |
58 | webpack "4.23.1" | 56 | webpack "4.29.0" |
59 | webpack-dev-middleware "3.4.0" | 57 | webpack-dev-middleware "3.5.1" |
60 | webpack-dev-server "3.1.10" | 58 | webpack-dev-server "3.1.14" |
61 | webpack-merge "4.1.4" | 59 | webpack-merge "4.2.1" |
62 | webpack-sources "1.3.0" | 60 | webpack-sources "1.3.0" |
63 | webpack-subresource-integrity "1.1.0-rc.6" | 61 | webpack-subresource-integrity "1.1.0-rc.6" |
64 | optionalDependencies: | 62 | optionalDependencies: |
65 | node-sass "4.10.0" | 63 | node-sass "4.11.0" |
66 | 64 | ||
67 | "@angular-devkit/build-optimizer@0.11.1": | 65 | "@angular-devkit/build-optimizer@0.13.1": |
68 | version "0.11.1" | 66 | version "0.13.1" |
69 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.11.1.tgz#1079737a44b26b39e35cea7f966a39cd11bbbf48" | 67 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.13.1.tgz#56151179bd6427918ba8e0f5a7e581e5daa00294" |
70 | integrity sha512-pyFP6ykZf8Iq8nRkgP2XKq8knpIG6ye0qYklnBC9815AC5RAO126Y4fmtd6tnH+5p1mQxnt5HegG0j5xOCgDRw== | 68 | integrity sha512-LmvHiI3H451aVWY5Ac6Fqz0i1eX/mUfWN+uJvo8NaL6Jc0HKYX2o3l4ODr8UUECWWctUC9AMD522ZMwAvnvsKQ== |
71 | dependencies: | 69 | dependencies: |
72 | loader-utils "1.1.0" | 70 | loader-utils "1.2.3" |
73 | source-map "0.5.6" | 71 | source-map "0.5.6" |
74 | typescript "3.1.6" | 72 | typescript "3.2.4" |
75 | webpack-sources "1.2.0" | 73 | webpack-sources "1.3.0" |
76 | 74 | ||
77 | "@angular-devkit/build-webpack@0.11.1": | 75 | "@angular-devkit/build-webpack@0.13.1": |
78 | version "0.11.1" | 76 | version "0.13.1" |
79 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.11.1.tgz#bd98ff3dea633c5b77671b471e72cf6c91f6c679" | 77 | resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.13.1.tgz#98d666765705e9379c9b2e0a3b6dfcd0347a2a32" |
80 | integrity sha512-p7fPHOi2Wfq2VPtnRVowg3n99MujghpOp6zW0gBJQD1TQhGVzPK6AX42S0NA4d05ahNBCDU2n7Y+5TjNJRIGJw== | 78 | integrity sha512-OGwC7bAl3u+w7Glw+OqIrN7OD1BkDXgrWbeQSpKAmsx6VdNPCnI4NPS+JldWNp70LVlE2nQlJUhtEqMVfBMnlg== |
81 | dependencies: | 79 | dependencies: |
82 | "@angular-devkit/architect" "0.11.1" | 80 | "@angular-devkit/architect" "0.13.1" |
83 | "@angular-devkit/core" "7.1.1" | 81 | "@angular-devkit/core" "7.3.1" |
84 | rxjs "6.3.3" | 82 | rxjs "6.3.3" |
85 | 83 | ||
86 | "@angular-devkit/core@7.1.1": | 84 | "@angular-devkit/core@7.3.1": |
87 | version "7.1.1" | 85 | version "7.3.1" |
88 | resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-7.1.1.tgz#ce0a674f16188072988502cc3f073b15efcfe194" | 86 | resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-7.3.1.tgz#d92f6545796579cabdcfc29579a2c977f7a96c6c" |
89 | integrity sha512-rODqECpOiV6vX+L1qd63GLiF3SG+V1O+d8WYtnKPOxnsMM9yWpWmqmroHtXfisjucu/zwoqj8HoO/noJZCfynw== | 87 | integrity sha512-56XDWWfIzOAkEk69lBLgmCYybPUA4yjunhmMlCk7vVdb7gbQUyzNjFD04Uj0GjlejatAQ5F76tRwygD9C+3RXQ== |
90 | dependencies: | 88 | dependencies: |
91 | ajv "6.5.3" | 89 | ajv "6.7.0" |
92 | chokidar "2.0.4" | 90 | chokidar "2.0.4" |
93 | fast-json-stable-stringify "2.0.0" | 91 | fast-json-stable-stringify "2.0.0" |
94 | rxjs "6.3.3" | 92 | rxjs "6.3.3" |
95 | source-map "0.7.3" | 93 | source-map "0.7.3" |
96 | 94 | ||
97 | "@angular-devkit/schematics@7.1.1": | 95 | "@angular-devkit/schematics@7.3.1": |
98 | version "7.1.1" | 96 | version "7.3.1" |
99 | resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-7.1.1.tgz#328ec6071c5ef3b1588a9f4bc97f5edfc3620b09" | 97 | resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-7.3.1.tgz#7dc704005b966ea6c1ee62f380120183bb76eee6" |
100 | integrity sha512-yjzTw8ZWMPg0Fc9VQCHNpUCAH7aiNxrUDs0IbhdC0CyKTBoqH+cx2xP4Z6ECf4uNwceLKJlE0l3ot42Ypnlziw== | 98 | integrity sha512-cd7usiasfSgw75INz72/VssrLr9tiVRYfo1TEdvr9ww0GuQbuQpB33xbV8W135eAV8+wzQ3Ce8ohaDHibvj6Yg== |
101 | dependencies: | 99 | dependencies: |
102 | "@angular-devkit/core" "7.1.1" | 100 | "@angular-devkit/core" "7.3.1" |
103 | rxjs "6.3.3" | 101 | rxjs "6.3.3" |
104 | 102 | ||
105 | "@angular/animations@~7.1.1": | 103 | "@angular/animations@~7.2.4": |
106 | version "7.1.1" | 104 | version "7.2.4" |
107 | resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.1.1.tgz#8fecbd19417364946a9ea40c8fdf32462110232f" | 105 | resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.4.tgz#4d0a0b9f14d6bfc38ca773613b61729d020435e6" |
108 | integrity sha512-iTNxhPPraCZsE4rgM23lguT1kDV4mfYAr+Bsi5J0+v9ZJA+VaKvi6eRW8ZGrx4/rDz6hzTnBn1jgPppHFbsOcw== | 106 | integrity sha512-Wx6cqU6koFOASlyl4aCygtbtROoehU6OKwV2EZTkfzHx6Eu/QyTiSa5kyoApVM5LMmCNeb8SxJMSAnKXztNl0A== |
109 | dependencies: | 107 | dependencies: |
110 | tslib "^1.9.0" | 108 | tslib "^1.9.0" |
111 | 109 | ||
112 | "@angular/cli@~7.1.1": | 110 | "@angular/cli@~7.3.1": |
113 | version "7.1.1" | 111 | version "7.3.1" |
114 | resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-7.1.1.tgz#c5dd2b92c5c3391f20262b5e530813e4e2d31777" | 112 | resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-7.3.1.tgz#a18acdec84deb03a1fae79cae415bbc8f9c87ffa" |
115 | integrity sha512-lPVKsk035T5Ls0Mf83OngrNoLZu/ucZSjRLN/GWZK1O/YYVmb/dTgVl/a7HC+G480tWQ34nlqnCRbrP7sE9v7g== | 113 | integrity sha512-8EvXYRhTqTaTk5PKv7VZxIWJiyG51R9RC9gtpRFx4bbnurqBHdEUxGMmaRsGT8QDbfvVsWnuakE0eeW1CrfZAQ== |
116 | dependencies: | 114 | dependencies: |
117 | "@angular-devkit/architect" "0.11.1" | 115 | "@angular-devkit/architect" "0.13.1" |
118 | "@angular-devkit/core" "7.1.1" | 116 | "@angular-devkit/core" "7.3.1" |
119 | "@angular-devkit/schematics" "7.1.1" | 117 | "@angular-devkit/schematics" "7.3.1" |
120 | "@schematics/angular" "7.1.1" | 118 | "@schematics/angular" "7.3.1" |
121 | "@schematics/update" "0.11.1" | 119 | "@schematics/update" "0.13.1" |
122 | inquirer "6.2.0" | 120 | "@yarnpkg/lockfile" "1.1.0" |
123 | opn "5.3.0" | 121 | ini "1.3.5" |
124 | semver "5.5.1" | 122 | inquirer "6.2.1" |
123 | npm-package-arg "6.1.0" | ||
124 | opn "5.4.0" | ||
125 | pacote "9.4.0" | ||
126 | semver "5.6.0" | ||
125 | symbol-observable "1.2.0" | 127 | symbol-observable "1.2.0" |
126 | 128 | ||
127 | "@angular/common@~7.1.1": | 129 | "@angular/common@~7.2.4": |
128 | version "7.1.1" | 130 | version "7.2.4" |
129 | resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.1.1.tgz#f78f884614ef81ab2fd648f1aa3e83aae370a6c8" | 131 | resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.4.tgz#9f1ed530e5dc7613a263e015c203ead390d50336" |
130 | integrity sha512-SngekFx9v39sjgi9pON0Wehxpu+NdUk7OEebw4Fa8dKqTgydTkuhmnNH+9WQe264asoeCt51oufPRjIqMLNohA== | 132 | integrity sha512-3/i8RtnLTx/90gJHk5maE8zwsSiHgHvLItaa0qVfNlWiU0eCId/PL6TgDkut5vN9SQYL0oxhxFaVd35HmwsmuQ== |
131 | dependencies: | 133 | dependencies: |
132 | tslib "^1.9.0" | 134 | tslib "^1.9.0" |
133 | 135 | ||
134 | "@angular/compiler-cli@~7.1.1": | 136 | "@angular/compiler-cli@~7.2.4": |
135 | version "7.1.1" | 137 | version "7.2.4" |
136 | resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.1.1.tgz#c5f6225fb72b56f42fa78c332fdee9755c64604e" | 138 | resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.2.4.tgz#3de23fd5f558a859a444c58dab18f2981c9c2937" |
137 | integrity sha512-4NXlkDhOEQgaP3Agigqw93CvXJvsfnXa0xiglq9e/wjL+6XbtM9WcDb5lfRQz41N9RSkO3pEHGvKMweKZGgogA== | 139 | integrity sha512-UhLosSeuwFIfaGqGcYOh9WSOuzEpeuhIRAOt81MeqOQEqkoreUjfxrQq8XWNkdqsPZHtiptF5ZwXlMBxlj9jJg== |
138 | dependencies: | 140 | dependencies: |
139 | canonical-path "1.0.0" | 141 | canonical-path "1.0.0" |
140 | chokidar "^1.4.2" | 142 | chokidar "^1.4.2" |
@@ -148,64 +150,64 @@ | |||
148 | tslib "^1.9.0" | 150 | tslib "^1.9.0" |
149 | yargs "9.0.1" | 151 | yargs "9.0.1" |
150 | 152 | ||
151 | "@angular/compiler@~7.1.1": | 153 | "@angular/compiler@~7.2.4": |
152 | version "7.1.1" | 154 | version "7.2.4" |
153 | resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.1.1.tgz#4efbcad27ab43d4cd36d936a8df2e073f6d02d0a" | 155 | resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.4.tgz#133eb97fc3169ec9ff84f134eb9e3497fa37537e" |
154 | integrity sha512-oJvBe8XZ+DXF/W/DxWBTbBcixJTuPeZWdkcZIGWhJoQP7K5GnGnj8ffP9Lp6Dh4TKv85awtC6OfIKhbHxa650Q== | 156 | integrity sha512-+zyMzPCL45ePEV9nrnYJvhAVgp2Y19bDaq0f0YdZAqAjgDqHzXGGR6wX8GueyJWmUYWx5vwK6Apla4HwDrYA1w== |
155 | dependencies: | 157 | dependencies: |
156 | tslib "^1.9.0" | 158 | tslib "^1.9.0" |
157 | 159 | ||
158 | "@angular/core@~7.1.1": | 160 | "@angular/core@~7.2.4": |
159 | version "7.1.1" | 161 | version "7.2.4" |
160 | resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.1.1.tgz#9748b0103cd86226554e1ccbd0f43dd8c46f1ed1" | 162 | resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.4.tgz#a6c84940c8edcfa37158f666a1f99c6e4a97bf95" |
161 | integrity sha512-Osig5SRgDRQ+Hec/liN7nq/BCJieB+4/pqRh9rFbOXezb2ptgRZqdXOXN8P17i4AwPVf308Mh55V0niJ5Eu3Rw== | 163 | integrity sha512-kfAxhIxl89PmB7y81FR/RAv0yWRFcEYxEnTwV+o8jKGfemAXtQ0g/Vh+lJR0SD/TBgFilMxotN1mhwH4A8GShw== |
162 | dependencies: | 164 | dependencies: |
163 | tslib "^1.9.0" | 165 | tslib "^1.9.0" |
164 | 166 | ||
165 | "@angular/forms@~7.1.1": | 167 | "@angular/forms@~7.2.4": |
166 | version "7.1.1" | 168 | version "7.2.4" |
167 | resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.1.1.tgz#d16ef10a901c007062fd19144cd77917ef55ee24" | 169 | resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.4.tgz#be89cf83ad16fa3c813c12e4cff85da5409cf7a0" |
168 | integrity sha512-yCWuPjpu23Wc3XUw7v/ACNn/e249oT0bYlM8aaMQ1F5OwrmmC4NJC12Rpl9Ihza61RIHIKzNcHVEgzc7WhcSag== | 170 | integrity sha512-DAtOrdlTRsgvmZrsvczCAkY8dhTwZb5DXBmPuSXh0UR9lvEiCgNHGbwEiIiIkAHpw1wSeXZrq0qyy/oJRvf18g== |
169 | dependencies: | 171 | dependencies: |
170 | tslib "^1.9.0" | 172 | tslib "^1.9.0" |
171 | 173 | ||
172 | "@angular/http@~7.1.1": | 174 | "@angular/http@~7.2.4": |
173 | version "7.1.1" | 175 | version "7.2.4" |
174 | resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.1.1.tgz#f19f17ad42e7f3cdabcf1250ca757640d0f02219" | 176 | resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.2.4.tgz#fc151ac15c8c7542cb7242a430ad2319655c2ff5" |
175 | integrity sha512-pRk+c/kz9aJ8te5xzCxlPLpFnwB0d/E9YkOo3/ydaXF9vZw13RTzk00YyzJ41PDzJf8oPDdXtueTQ+vtJ7Srtw== | 177 | integrity sha512-kazJREm7MtSCYbE+9zU/CcUXI5Csu53PooeQlAp80/TOHqry6fVKIMHCI892Db9ScY2ds0SzbyTmrxEQo7PP1A== |
176 | dependencies: | 178 | dependencies: |
177 | tslib "^1.9.0" | 179 | tslib "^1.9.0" |
178 | 180 | ||
179 | "@angular/language-service@~7.1.1": | 181 | "@angular/language-service@~7.2.4": |
180 | version "7.1.1" | 182 | version "7.2.4" |
181 | resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-7.1.1.tgz#6bbe35b2430ad54618a1803f881efb5894b296c9" | 183 | resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-7.2.4.tgz#db72460040b070410cbff678410c142f4d682af8" |
182 | integrity sha512-X+5g20PMtNRGZIa3svMv4PLJdJehn4wqrS8nwOtzH5XkSn5vA3IxjsJVdSzAy2AN0/sKKJK5jmQorPtKO4saJg== | 184 | integrity sha512-A9Rud/27hHMSUUjpgn57nVeLsoYgdvFwJhtlZA/oCuSpmlD+LqqBsEpPhivwn++u44+DSrFXsic29jlFnsBotw== |
183 | 185 | ||
184 | "@angular/platform-browser-dynamic@~7.1.1": | 186 | "@angular/platform-browser-dynamic@~7.2.4": |
185 | version "7.1.1" | 187 | version "7.2.4" |
186 | resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.1.1.tgz#6945298446173338782f437a996226110cda0d3e" | 188 | resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.4.tgz#24dce1bb0d9dab541b3b1b3eda3084a732f11b64" |
187 | integrity sha512-ZIu48Vn4S6gjD7CMbGlKGaPQ8v9rYkWzlNYi4vTYzgiqKKNC3hqLsVESU3mSvr5oeQBxSIBidTdHSyafHFrA2w== | 189 | integrity sha512-J/xWlmaYOPUoCHZ5TiIRiyYa4uRMtCz3aGdBfY8k/NWtNo8SCYaS3aut7Sk4RS5rK8aAVi+aYFlY5YOrlW+Hbg== |
188 | dependencies: | 190 | dependencies: |
189 | tslib "^1.9.0" | 191 | tslib "^1.9.0" |
190 | 192 | ||
191 | "@angular/platform-browser@~7.1.1": | 193 | "@angular/platform-browser@~7.2.4": |
192 | version "7.1.1" | 194 | version "7.2.4" |
193 | resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.1.1.tgz#a6bd408f656dc43ee5a2d8af3dfaa786c7c1dfca" | 195 | resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.4.tgz#2cf5305878d0620d6b8c02eff00ac3ca8dbc5970" |
194 | integrity sha512-I6OPjecynGJSbPtzu0gvEgSmIR6X6/xEAhg4L9PycW1ryjzptTC9klWRTWIqsIBqMxhVnY44uKLeRNrDwMOwyA== | 196 | integrity sha512-Klt8aKR5SP9bqfMfpSY5vQOY7AQEs8JGuZOk5Bfc2dUtYT2IEIvK2IqO8v2rcFRVO13HOPUxl328efyHqLgI7g== |
195 | dependencies: | 197 | dependencies: |
196 | tslib "^1.9.0" | 198 | tslib "^1.9.0" |
197 | 199 | ||
198 | "@angular/router@~7.1.1": | 200 | "@angular/router@~7.2.4": |
199 | version "7.1.1" | 201 | version "7.2.4" |
200 | resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.1.1.tgz#80a4cdffc03a529b73485c2ad63a30ec435364ea" | 202 | resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.2.4.tgz#83f1997c2a4e6acda93b991b8d7f3dad2b3f91f0" |
201 | integrity sha512-jbnqEq/1iDBkeH8Vn13hauGPTzhwllWM+MLfmdNGTiMzGRx4pmkWa57seDOeBF/GNYBL9JjkWTCrkKFAc2FJKw== | 203 | integrity sha512-T8Uqf2H1SV1MQI38WwYJ4aa+4NNnvlp2Tp/rkfg6tKcp/cLkKqE6OOfiy9lmW+i/624v8tMgYoBMOUNBjAG23g== |
202 | dependencies: | 204 | dependencies: |
203 | tslib "^1.9.0" | 205 | tslib "^1.9.0" |
204 | 206 | ||
205 | "@angular/service-worker@~7.1.1": | 207 | "@angular/service-worker@~7.2.4": |
206 | version "7.1.1" | 208 | version "7.2.4" |
207 | resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-7.1.1.tgz#c9e6f0265d7e102d8271483519cf09a180f0e08b" | 209 | resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-7.2.4.tgz#d16d6d08c0d5c29c93e9f80cddc4013c9b8859fb" |
208 | integrity sha512-xX00x0XMW47jEfYTZLwdJCqkmPE7+mdtlSeOGpjaKv6Y2hqZodz80RYgH5JltM4RKEzOvQolR6KmdKcw1ANs9Q== | 210 | integrity sha512-IYsHshkgCYYmWLwtP7wwk8tfwphE4IJrkUitEu+ST6x+er/K9LyLo09WQeEZHIwDaPm9icoqc3TJJdXI46mrmg== |
209 | dependencies: | 211 | dependencies: |
210 | tslib "^1.9.0" | 212 | tslib "^1.9.0" |
211 | 213 | ||
@@ -321,16 +323,16 @@ | |||
321 | dependencies: | 323 | dependencies: |
322 | tslib "^1.9.0" | 324 | tslib "^1.9.0" |
323 | 325 | ||
324 | "@ngtools/webpack@7.1.1": | 326 | "@ngtools/webpack@7.3.1": |
325 | version "7.1.1" | 327 | version "7.3.1" |
326 | resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-7.1.1.tgz#c418e1cb0d70a77d06e8c32500fe2e92e606ea52" | 328 | resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-7.3.1.tgz#4ff68007fd68fdc26f22e19115182f96fb6f7335" |
327 | integrity sha512-XW/YDjiDZlwOYK4YvGAIKIVEkqtdwPLwTWAmDbnfpEHQc8UALsBrzGdjze0jSfXQdQxkbmXo0aolZgNc7uL/wQ== | 329 | integrity sha512-EGQRjgDf5XP+Fm1MdZNRFiPd9e1vhl11BhjkwqkAsewic4eoz6fqXfj/Osz1hQy8xU+2dPPf/byQ/+nY3E02Zg== |
328 | dependencies: | 330 | dependencies: |
329 | "@angular-devkit/core" "7.1.1" | 331 | "@angular-devkit/core" "7.3.1" |
330 | enhanced-resolve "4.1.0" | 332 | enhanced-resolve "4.1.0" |
331 | rxjs "6.3.3" | 333 | rxjs "6.3.3" |
332 | tree-kill "1.2.0" | 334 | tree-kill "1.2.1" |
333 | webpack-sources "1.2.0" | 335 | webpack-sources "1.3.0" |
334 | 336 | ||
335 | "@ngx-loading-bar/core@3.0.0", "@ngx-loading-bar/core@^3.0.0": | 337 | "@ngx-loading-bar/core@3.0.0", "@ngx-loading-bar/core@^3.0.0": |
336 | version "3.0.0" | 338 | version "3.0.0" |
@@ -371,29 +373,34 @@ | |||
371 | tslib "^1.9.0" | 373 | tslib "^1.9.0" |
372 | yargs "10.0.3" | 374 | yargs "10.0.3" |
373 | 375 | ||
374 | "@schematics/angular@7.1.1": | 376 | "@schematics/angular@7.3.1": |
375 | version "7.1.1" | 377 | version "7.3.1" |
376 | resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-7.1.1.tgz#4ee17a17d221eaf48009db0b991766d1074d0b4f" | 378 | resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-7.3.1.tgz#6fcd7004210fa9305310c3109c084df5c5521776" |
377 | integrity sha512-jMaj8y3rNTQQXuH38uoWfAOmwYjtzqo1RelNfACnT54mfO/Dat+k7WasBLHWuvzvnN4/Ga3kXL7sJpkeMciiIg== | 379 | integrity sha512-0Ne8APPlTAjKg5CSZqluwCuW/5yPjr3ALCWzqwPxN0suE745usThtasBmqrjw0RMIt8nRqRgtg54Z7lCPO9ZFg== |
378 | dependencies: | 380 | dependencies: |
379 | "@angular-devkit/core" "7.1.1" | 381 | "@angular-devkit/core" "7.3.1" |
380 | "@angular-devkit/schematics" "7.1.1" | 382 | "@angular-devkit/schematics" "7.3.1" |
381 | typescript "3.1.6" | 383 | typescript "3.2.4" |
382 | 384 | ||
383 | "@schematics/update@0.11.1": | 385 | "@schematics/update@0.13.1": |
384 | version "0.11.1" | 386 | version "0.13.1" |
385 | resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.11.1.tgz#5129a800043dc38ee1f1c879865e0df82ddac7ed" | 387 | resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.13.1.tgz#481475aee18b4a9472a06512b2e4d6429af68231" |
386 | integrity sha512-IzPXamoMpDb2eY2zSW4fPuuH+7RfJLte9XVzQM2y3ZTBhlJQFLqx7qJtOXdcXUboonC6o61KCayNDERFnDUdPg== | 388 | integrity sha512-EHOqolT/d/jRGuVTCUESLpk8JNpuaPlsVHfeK7Kdp/t0wSEnmtOelZX4+leS25lGXDaDUF3138ntjrZR4n6bGw== |
387 | dependencies: | 389 | dependencies: |
388 | "@angular-devkit/core" "7.1.1" | 390 | "@angular-devkit/core" "7.3.1" |
389 | "@angular-devkit/schematics" "7.1.1" | 391 | "@angular-devkit/schematics" "7.3.1" |
390 | "@yarnpkg/lockfile" "1.1.0" | 392 | "@yarnpkg/lockfile" "1.1.0" |
391 | ini "1.3.5" | 393 | ini "1.3.5" |
392 | pacote "9.1.1" | 394 | pacote "9.4.0" |
393 | rxjs "6.3.3" | 395 | rxjs "6.3.3" |
394 | semver "5.5.1" | 396 | semver "5.6.0" |
395 | semver-intersect "1.4.0" | 397 | semver-intersect "1.4.0" |
396 | 398 | ||
399 | "@streamroot/videojs-hlsjs-plugin@^1.0.7": | ||
400 | version "1.0.7" | ||
401 | resolved "https://registry.yarnpkg.com/@streamroot/videojs-hlsjs-plugin/-/videojs-hlsjs-plugin-1.0.7.tgz#581aecdf6a966162b404c60bd3ab8264eb89d334" | ||
402 | integrity sha512-7oAIOhEFxkfLOYWDfg7Oh3+OrnoTElRvUE3Jblg2B+SHmnrw4YXQnAwYJ0AHjNIBKoHnQubzZGttLaHAFJVspQ== | ||
403 | |||
397 | "@types/bittorrent-protocol@*": | 404 | "@types/bittorrent-protocol@*": |
398 | version "2.2.2" | 405 | version "2.2.2" |
399 | resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.2.tgz#169e9633e1bd18e6b830d11cf42e611b1972cb83" | 406 | resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.2.tgz#169e9633e1bd18e6b830d11cf42e611b1972cb83" |
@@ -406,6 +413,11 @@ | |||
406 | resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.0.tgz#35cc282488de6f10af1d92902899a3b8ca3fbc47" | 413 | resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.0.tgz#35cc282488de6f10af1d92902899a3b8ca3fbc47" |
407 | integrity sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ== | 414 | integrity sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ== |
408 | 415 | ||
416 | "@types/hls.js@^0.12.0": | ||
417 | version "0.12.0" | ||
418 | resolved "https://registry.yarnpkg.com/@types/hls.js/-/hls.js-0.12.0.tgz#33f73e542201a766fa56792cb81fe9f97d7097ed" | ||
419 | integrity sha512-hJ7eJAQVEazAANK4Ay0YbXlZF36SDy9c8kcHTF7//77ylgV6hV/JrlwhVmobsSacr5aZcbw5MbZ2bSHbS36eOQ== | ||
420 | |||
409 | "@types/jasmine@*": | 421 | "@types/jasmine@*": |
410 | version "3.3.1" | 422 | version "3.3.1" |
411 | resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.3.1.tgz#b6c4f356013364e98b583647c7b3b6de6fccd2cc" | 423 | resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.3.1.tgz#b6c4f356013364e98b583647c7b3b6de6fccd2cc" |
@@ -515,11 +527,25 @@ | |||
515 | resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14" | 527 | resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14" |
516 | integrity sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg== | 528 | integrity sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg== |
517 | 529 | ||
530 | "@types/source-list-map@*": | ||
531 | version "0.1.2" | ||
532 | resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" | ||
533 | integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== | ||
534 | |||
518 | "@types/video.js@^7.2.5": | 535 | "@types/video.js@^7.2.5": |
519 | version "7.2.5" | 536 | version "7.2.5" |
520 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.2.5.tgz#20896c81141d3517c3a89bb6eb97c6a191aa5d4c" | 537 | resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.2.5.tgz#20896c81141d3517c3a89bb6eb97c6a191aa5d4c" |
521 | integrity sha512-5WUDOme0q81d58nEqf7qnz7B2Jc4jlA7/MQGOgoqI5VE6oied0KUfk5x/XqPSZSAHNwDDREAkrcK8JXcB+iruQ== | 538 | integrity sha512-5WUDOme0q81d58nEqf7qnz7B2Jc4jlA7/MQGOgoqI5VE6oied0KUfk5x/XqPSZSAHNwDDREAkrcK8JXcB+iruQ== |
522 | 539 | ||
540 | "@types/webpack-sources@^0.1.5": | ||
541 | version "0.1.5" | ||
542 | resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" | ||
543 | integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== | ||
544 | dependencies: | ||
545 | "@types/node" "*" | ||
546 | "@types/source-list-map" "*" | ||
547 | source-map "^0.6.1" | ||
548 | |||
523 | "@types/webtorrent@^0.98.4": | 549 | "@types/webtorrent@^0.98.4": |
524 | version "0.98.4" | 550 | version "0.98.4" |
525 | resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.98.4.tgz#cf8dbe22e3d5cf6915305f7f970b52bca01bf8b4" | 551 | resolved "https://registry.yarnpkg.com/@types/webtorrent/-/webtorrent-0.98.4.tgz#cf8dbe22e3d5cf6915305f7f970b52bca01bf8b4" |
@@ -543,15 +569,6 @@ | |||
543 | url-toolkit "^2.1.3" | 569 | url-toolkit "^2.1.3" |
544 | video.js "^6.8.0 || ^7.0.0" | 570 | video.js "^6.8.0 || ^7.0.0" |
545 | 571 | ||
546 | "@webassemblyjs/ast@1.7.10": | ||
547 | version "1.7.10" | ||
548 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.10.tgz#0cfc61d61286240b72fc522cb755613699eea40a" | ||
549 | integrity sha512-wTUeaByYN2EA6qVqhbgavtGc7fLTOx0glG2IBsFlrFG51uXIGlYBTyIZMf4SPLo3v1bgV/7lBN3l7Z0R6Hswew== | ||
550 | dependencies: | ||
551 | "@webassemblyjs/helper-module-context" "1.7.10" | ||
552 | "@webassemblyjs/helper-wasm-bytecode" "1.7.10" | ||
553 | "@webassemblyjs/wast-parser" "1.7.10" | ||
554 | |||
555 | "@webassemblyjs/ast@1.7.11": | 572 | "@webassemblyjs/ast@1.7.11": |
556 | version "1.7.11" | 573 | version "1.7.11" |
557 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace" | 574 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace" |
@@ -561,43 +578,21 @@ | |||
561 | "@webassemblyjs/helper-wasm-bytecode" "1.7.11" | 578 | "@webassemblyjs/helper-wasm-bytecode" "1.7.11" |
562 | "@webassemblyjs/wast-parser" "1.7.11" | 579 | "@webassemblyjs/wast-parser" "1.7.11" |
563 | 580 | ||
564 | "@webassemblyjs/floating-point-hex-parser@1.7.10": | ||
565 | version "1.7.10" | ||
566 | resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.10.tgz#ee63d729c6311a85863e369a473f9983f984e4d9" | ||
567 | integrity sha512-gMsGbI6I3p/P1xL2UxqhNh1ga2HCsx5VBB2i5VvJFAaqAjd2PBTRULc3BpTydabUQEGlaZCzEUQhLoLG7TvEYQ== | ||
568 | |||
569 | "@webassemblyjs/floating-point-hex-parser@1.7.11": | 581 | "@webassemblyjs/floating-point-hex-parser@1.7.11": |
570 | version "1.7.11" | 582 | version "1.7.11" |
571 | resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313" | 583 | resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313" |
572 | integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg== | 584 | integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg== |
573 | 585 | ||
574 | "@webassemblyjs/helper-api-error@1.7.10": | ||
575 | version "1.7.10" | ||
576 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.10.tgz#bfcb3bbe59775357475790a2ad7b289f09b2f198" | ||
577 | integrity sha512-DoYRlPWtuw3yd5BOr9XhtrmB6X1enYF0/54yNvQWGXZEPDF5PJVNI7zQ7gkcKfTESzp8bIBWailaFXEK/jjCsw== | ||
578 | |||
579 | "@webassemblyjs/helper-api-error@1.7.11": | 586 | "@webassemblyjs/helper-api-error@1.7.11": |
580 | version "1.7.11" | 587 | version "1.7.11" |
581 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a" | 588 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a" |
582 | integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg== | 589 | integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg== |
583 | 590 | ||
584 | "@webassemblyjs/helper-buffer@1.7.10": | ||
585 | version "1.7.10" | ||
586 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.10.tgz#0a8c624c67ad0b214d2e003859921a1988cb151b" | ||
587 | integrity sha512-+RMU3dt/dPh4EpVX4u5jxsOlw22tp3zjqE0m3ftU2tsYxnPULb4cyHlgaNd2KoWuwasCQqn8Mhr+TTdbtj3LlA== | ||
588 | |||
589 | "@webassemblyjs/helper-buffer@1.7.11": | 591 | "@webassemblyjs/helper-buffer@1.7.11": |
590 | version "1.7.11" | 592 | version "1.7.11" |
591 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b" | 593 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b" |
592 | integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w== | 594 | integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w== |
593 | 595 | ||
594 | "@webassemblyjs/helper-code-frame@1.7.10": | ||
595 | version "1.7.10" | ||
596 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.10.tgz#0ab7e22fad0241a173178c73976fc0edf50832ce" | ||
597 | integrity sha512-UiytbpKAULOEab2hUZK2ywXen4gWJVrgxtwY3Kn+eZaaSWaRM8z/7dAXRSoamhKFiBh1uaqxzE/XD9BLlug3gw== | ||
598 | dependencies: | ||
599 | "@webassemblyjs/wast-printer" "1.7.10" | ||
600 | |||
601 | "@webassemblyjs/helper-code-frame@1.7.11": | 596 | "@webassemblyjs/helper-code-frame@1.7.11": |
602 | version "1.7.11" | 597 | version "1.7.11" |
603 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b" | 598 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b" |
@@ -605,46 +600,21 @@ | |||
605 | dependencies: | 600 | dependencies: |
606 | "@webassemblyjs/wast-printer" "1.7.11" | 601 | "@webassemblyjs/wast-printer" "1.7.11" |
607 | 602 | ||
608 | "@webassemblyjs/helper-fsm@1.7.10": | ||
609 | version "1.7.10" | ||
610 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.10.tgz#0915e7713fbbb735620a9d3e4fa3d7951f97ac64" | ||
611 | integrity sha512-w2vDtUK9xeSRtt5+RnnlRCI7wHEvLjF0XdnxJpgx+LJOvklTZPqWkuy/NhwHSLP19sm9H8dWxKeReMR7sCkGZA== | ||
612 | |||
613 | "@webassemblyjs/helper-fsm@1.7.11": | 603 | "@webassemblyjs/helper-fsm@1.7.11": |
614 | version "1.7.11" | 604 | version "1.7.11" |
615 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181" | 605 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181" |
616 | integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A== | 606 | integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A== |
617 | 607 | ||
618 | "@webassemblyjs/helper-module-context@1.7.10": | ||
619 | version "1.7.10" | ||
620 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.10.tgz#9beb83f72740f5ac8075313b5cac5e796510f755" | ||
621 | integrity sha512-yE5x/LzZ3XdPdREmJijxzfrf+BDRewvO0zl8kvORgSWmxpRrkqY39KZSq6TSgIWBxkK4SrzlS3BsMCv2s1FpsQ== | ||
622 | |||
623 | "@webassemblyjs/helper-module-context@1.7.11": | 608 | "@webassemblyjs/helper-module-context@1.7.11": |
624 | version "1.7.11" | 609 | version "1.7.11" |
625 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209" | 610 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209" |
626 | integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg== | 611 | integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg== |
627 | 612 | ||
628 | "@webassemblyjs/helper-wasm-bytecode@1.7.10": | ||
629 | version "1.7.10" | ||
630 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.10.tgz#797b1e734bbcfdea8399669cdc58308ef1c7ffc0" | ||
631 | integrity sha512-u5qy4SJ/OrxKxZqJ9N3qH4ZQgHaAzsopsYwLvoWJY6Q33r8PhT3VPyNMaJ7ZFoqzBnZlCcS/0f4Sp8WBxylXfg== | ||
632 | |||
633 | "@webassemblyjs/helper-wasm-bytecode@1.7.11": | 613 | "@webassemblyjs/helper-wasm-bytecode@1.7.11": |
634 | version "1.7.11" | 614 | version "1.7.11" |
635 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06" | 615 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06" |
636 | integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ== | 616 | integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ== |
637 | 617 | ||
638 | "@webassemblyjs/helper-wasm-section@1.7.10": | ||
639 | version "1.7.10" | ||
640 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.10.tgz#c0ea3703c615d7bc3e3507c3b7991c8767b2f20e" | ||
641 | integrity sha512-Ecvww6sCkcjatcyctUrn22neSJHLN/TTzolMGG/N7S9rpbsTZ8c6Bl98GpSpV77EvzNijiNRHBG0+JO99qKz6g== | ||
642 | dependencies: | ||
643 | "@webassemblyjs/ast" "1.7.10" | ||
644 | "@webassemblyjs/helper-buffer" "1.7.10" | ||
645 | "@webassemblyjs/helper-wasm-bytecode" "1.7.10" | ||
646 | "@webassemblyjs/wasm-gen" "1.7.10" | ||
647 | |||
648 | "@webassemblyjs/helper-wasm-section@1.7.11": | 618 | "@webassemblyjs/helper-wasm-section@1.7.11": |
649 | version "1.7.11" | 619 | version "1.7.11" |
650 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a" | 620 | resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a" |
@@ -655,13 +625,6 @@ | |||
655 | "@webassemblyjs/helper-wasm-bytecode" "1.7.11" | 625 | "@webassemblyjs/helper-wasm-bytecode" "1.7.11" |
656 | "@webassemblyjs/wasm-gen" "1.7.11" | 626 | "@webassemblyjs/wasm-gen" "1.7.11" |
657 | 627 | ||
658 | "@webassemblyjs/ieee754@1.7.10": | ||
659 | version "1.7.10" | ||
660 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.10.tgz#62c1728b7ef0f66ef8221e2966a0afd75db430df" | ||
661 | integrity sha512-HRcWcY+YWt4+s/CvQn+vnSPfRaD4KkuzQFt5MNaELXXHSjelHlSEA8ZcqT69q0GTIuLWZ6JaoKar4yWHVpZHsQ== | ||
662 | dependencies: | ||
663 | "@xtuc/ieee754" "^1.2.0" | ||
664 | |||
665 | "@webassemblyjs/ieee754@1.7.11": | 628 | "@webassemblyjs/ieee754@1.7.11": |
666 | version "1.7.11" | 629 | version "1.7.11" |
667 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b" | 630 | resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b" |
@@ -669,13 +632,6 @@ | |||
669 | dependencies: | 632 | dependencies: |
670 | "@xtuc/ieee754" "^1.2.0" | 633 | "@xtuc/ieee754" "^1.2.0" |
671 | 634 | ||
672 | "@webassemblyjs/leb128@1.7.10": | ||
673 | version "1.7.10" | ||
674 | resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.10.tgz#167e0bb4b06d7701585772a73fba9f4df85439f6" | ||
675 | integrity sha512-og8MciYlA8hvzCLR71hCuZKPbVBfLQeHv7ImKZ4nlyxrYbG7uJHYtHiHu6OV9SqrGuD03H/HtXC4Bgdjfm9FHw== | ||
676 | dependencies: | ||
677 | "@xtuc/long" "4.2.1" | ||
678 | |||
679 | "@webassemblyjs/leb128@1.7.11": | 635 | "@webassemblyjs/leb128@1.7.11": |
680 | version "1.7.11" | 636 | version "1.7.11" |
681 | resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" | 637 | resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" |
@@ -683,30 +639,11 @@ | |||
683 | dependencies: | 639 | dependencies: |
684 | "@xtuc/long" "4.2.1" | 640 | "@xtuc/long" "4.2.1" |
685 | 641 | ||
686 | "@webassemblyjs/utf8@1.7.10": | ||
687 | version "1.7.10" | ||
688 | resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.10.tgz#b6728f5b6f50364abc155be029f9670e6685605a" | ||
689 | integrity sha512-Ng6Pxv6siyZp635xCSnH3mKmIFgqWPCcGdoo0GBYgyGdxu7cUj4agV7Uu1a8REP66UYUFXJLudeGgd4RvuJAnQ== | ||
690 | |||
691 | "@webassemblyjs/utf8@1.7.11": | 642 | "@webassemblyjs/utf8@1.7.11": |
692 | version "1.7.11" | 643 | version "1.7.11" |
693 | resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82" | 644 | resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82" |
694 | integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA== | 645 | integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA== |
695 | 646 | ||
696 | "@webassemblyjs/wasm-edit@1.7.10": | ||
697 | version "1.7.10" | ||
698 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.10.tgz#83fe3140f5a58f5a30b914702be9f0e59a399092" | ||
699 | integrity sha512-e9RZFQlb+ZuYcKRcW9yl+mqX/Ycj9+3/+ppDI8nEE/NCY6FoK8f3dKBcfubYV/HZn44b+ND4hjh+4BYBt+sDnA== | ||
700 | dependencies: | ||
701 | "@webassemblyjs/ast" "1.7.10" | ||
702 | "@webassemblyjs/helper-buffer" "1.7.10" | ||
703 | "@webassemblyjs/helper-wasm-bytecode" "1.7.10" | ||
704 | "@webassemblyjs/helper-wasm-section" "1.7.10" | ||
705 | "@webassemblyjs/wasm-gen" "1.7.10" | ||
706 | "@webassemblyjs/wasm-opt" "1.7.10" | ||
707 | "@webassemblyjs/wasm-parser" "1.7.10" | ||
708 | "@webassemblyjs/wast-printer" "1.7.10" | ||
709 | |||
710 | "@webassemblyjs/wasm-edit@1.7.11": | 647 | "@webassemblyjs/wasm-edit@1.7.11": |
711 | version "1.7.11" | 648 | version "1.7.11" |
712 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005" | 649 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005" |
@@ -721,17 +658,6 @@ | |||
721 | "@webassemblyjs/wasm-parser" "1.7.11" | 658 | "@webassemblyjs/wasm-parser" "1.7.11" |
722 | "@webassemblyjs/wast-printer" "1.7.11" | 659 | "@webassemblyjs/wast-printer" "1.7.11" |
723 | 660 | ||
724 | "@webassemblyjs/wasm-gen@1.7.10": | ||
725 | version "1.7.10" | ||
726 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.10.tgz#4de003806ae29c97ab3707782469b53299570174" | ||
727 | integrity sha512-M0lb6cO2Y0PzDye/L39PqwV+jvO+2YxEG5ax+7dgq7EwXdAlpOMx1jxyXJTScQoeTpzOPIb+fLgX/IkLF8h2yw== | ||
728 | dependencies: | ||
729 | "@webassemblyjs/ast" "1.7.10" | ||
730 | "@webassemblyjs/helper-wasm-bytecode" "1.7.10" | ||
731 | "@webassemblyjs/ieee754" "1.7.10" | ||
732 | "@webassemblyjs/leb128" "1.7.10" | ||
733 | "@webassemblyjs/utf8" "1.7.10" | ||
734 | |||
735 | "@webassemblyjs/wasm-gen@1.7.11": | 661 | "@webassemblyjs/wasm-gen@1.7.11": |
736 | version "1.7.11" | 662 | version "1.7.11" |
737 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8" | 663 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8" |
@@ -743,16 +669,6 @@ | |||
743 | "@webassemblyjs/leb128" "1.7.11" | 669 | "@webassemblyjs/leb128" "1.7.11" |
744 | "@webassemblyjs/utf8" "1.7.11" | 670 | "@webassemblyjs/utf8" "1.7.11" |
745 | 671 | ||
746 | "@webassemblyjs/wasm-opt@1.7.10": | ||
747 | version "1.7.10" | ||
748 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.10.tgz#d151e31611934a556c82789fdeec41a814993c2a" | ||
749 | integrity sha512-R66IHGCdicgF5ZliN10yn5HaC7vwYAqrSVJGjtJJQp5+QNPBye6heWdVH/at40uh0uoaDN/UVUfXK0gvuUqtVg== | ||
750 | dependencies: | ||
751 | "@webassemblyjs/ast" "1.7.10" | ||
752 | "@webassemblyjs/helper-buffer" "1.7.10" | ||
753 | "@webassemblyjs/wasm-gen" "1.7.10" | ||
754 | "@webassemblyjs/wasm-parser" "1.7.10" | ||
755 | |||
756 | "@webassemblyjs/wasm-opt@1.7.11": | 672 | "@webassemblyjs/wasm-opt@1.7.11": |
757 | version "1.7.11" | 673 | version "1.7.11" |
758 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" | 674 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" |
@@ -763,18 +679,6 @@ | |||
763 | "@webassemblyjs/wasm-gen" "1.7.11" | 679 | "@webassemblyjs/wasm-gen" "1.7.11" |
764 | "@webassemblyjs/wasm-parser" "1.7.11" | 680 | "@webassemblyjs/wasm-parser" "1.7.11" |
765 | 681 | ||
766 | "@webassemblyjs/wasm-parser@1.7.10": | ||
767 | version "1.7.10" | ||
768 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.10.tgz#0367be7bf8f09e3e6abc95f8e483b9206487ec65" | ||
769 | integrity sha512-AEv8mkXVK63n/iDR3T693EzoGPnNAwKwT3iHmKJNBrrALAhhEjuPzo/lTE4U7LquEwyvg5nneSNdTdgrBaGJcA== | ||
770 | dependencies: | ||
771 | "@webassemblyjs/ast" "1.7.10" | ||
772 | "@webassemblyjs/helper-api-error" "1.7.10" | ||
773 | "@webassemblyjs/helper-wasm-bytecode" "1.7.10" | ||
774 | "@webassemblyjs/ieee754" "1.7.10" | ||
775 | "@webassemblyjs/leb128" "1.7.10" | ||
776 | "@webassemblyjs/utf8" "1.7.10" | ||
777 | |||
778 | "@webassemblyjs/wasm-parser@1.7.11": | 682 | "@webassemblyjs/wasm-parser@1.7.11": |
779 | version "1.7.11" | 683 | version "1.7.11" |
780 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a" | 684 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a" |
@@ -787,18 +691,6 @@ | |||
787 | "@webassemblyjs/leb128" "1.7.11" | 691 | "@webassemblyjs/leb128" "1.7.11" |
788 | "@webassemblyjs/utf8" "1.7.11" | 692 | "@webassemblyjs/utf8" "1.7.11" |
789 | 693 | ||
790 | "@webassemblyjs/wast-parser@1.7.10": | ||
791 | version "1.7.10" | ||
792 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.10.tgz#058f598b52f730b23fc874d4775b6286b6247264" | ||
793 | integrity sha512-YTPEtOBljkCL0VjDp4sHe22dAYSm3ZwdJ9+2NTGdtC7ayNvuip1wAhaAS8Zt9Q6SW9E5Jf5PX7YE3XWlrzR9cw== | ||
794 | dependencies: | ||
795 | "@webassemblyjs/ast" "1.7.10" | ||
796 | "@webassemblyjs/floating-point-hex-parser" "1.7.10" | ||
797 | "@webassemblyjs/helper-api-error" "1.7.10" | ||
798 | "@webassemblyjs/helper-code-frame" "1.7.10" | ||
799 | "@webassemblyjs/helper-fsm" "1.7.10" | ||
800 | "@xtuc/long" "4.2.1" | ||
801 | |||
802 | "@webassemblyjs/wast-parser@1.7.11": | 694 | "@webassemblyjs/wast-parser@1.7.11": |
803 | version "1.7.11" | 695 | version "1.7.11" |
804 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c" | 696 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c" |
@@ -811,15 +703,6 @@ | |||
811 | "@webassemblyjs/helper-fsm" "1.7.11" | 703 | "@webassemblyjs/helper-fsm" "1.7.11" |
812 | "@xtuc/long" "4.2.1" | 704 | "@xtuc/long" "4.2.1" |
813 | 705 | ||
814 | "@webassemblyjs/wast-printer@1.7.10": | ||
815 | version "1.7.10" | ||
816 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.10.tgz#d817909d2450ae96c66b7607624d98a33b84223b" | ||
817 | integrity sha512-mJ3QKWtCchL1vhU/kZlJnLPuQZnlDOdZsyP0bbLWPGdYsQDnSBvyTLhzwBA3QAMlzEL9V4JHygEmK6/OTEyytA== | ||
818 | dependencies: | ||
819 | "@webassemblyjs/ast" "1.7.10" | ||
820 | "@webassemblyjs/wast-parser" "1.7.10" | ||
821 | "@xtuc/long" "4.2.1" | ||
822 | |||
823 | "@webassemblyjs/wast-printer@1.7.11": | 706 | "@webassemblyjs/wast-printer@1.7.11": |
824 | version "1.7.11" | 707 | version "1.7.11" |
825 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813" | 708 | resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813" |
@@ -862,11 +745,6 @@ abbrev@1: | |||
862 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" | 745 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" |
863 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== | 746 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== |
864 | 747 | ||
865 | abbrev@1.0.x: | ||
866 | version "1.0.9" | ||
867 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" | ||
868 | integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= | ||
869 | |||
870 | accepts@~1.3.4, accepts@~1.3.5: | 748 | accepts@~1.3.4, accepts@~1.3.5: |
871 | version "1.3.5" | 749 | version "1.3.5" |
872 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" | 750 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" |
@@ -875,12 +753,10 @@ accepts@~1.3.4, accepts@~1.3.5: | |||
875 | mime-types "~2.1.18" | 753 | mime-types "~2.1.18" |
876 | negotiator "0.6.1" | 754 | negotiator "0.6.1" |
877 | 755 | ||
878 | acorn-dynamic-import@^3.0.0: | 756 | acorn-dynamic-import@^4.0.0: |
879 | version "3.0.0" | 757 | version "4.0.0" |
880 | resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" | 758 | resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" |
881 | integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg== | 759 | integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== |
882 | dependencies: | ||
883 | acorn "^5.0.0" | ||
884 | 760 | ||
885 | acorn-globals@^4.1.0: | 761 | acorn-globals@^4.1.0: |
886 | version "4.3.0" | 762 | version "4.3.0" |
@@ -895,7 +771,7 @@ acorn-walk@^6.0.1: | |||
895 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" | 771 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" |
896 | integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== | 772 | integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== |
897 | 773 | ||
898 | acorn@^5.0.0, acorn@^5.5.3, acorn@^5.6.2, acorn@^5.7.3: | 774 | acorn@^5.5.3, acorn@^5.7.3: |
899 | version "5.7.3" | 775 | version "5.7.3" |
900 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" | 776 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" |
901 | integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== | 777 | integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== |
@@ -905,6 +781,11 @@ acorn@^6.0.1: | |||
905 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" | 781 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" |
906 | integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg== | 782 | integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg== |
907 | 783 | ||
784 | acorn@^6.0.5: | ||
785 | version "6.1.0" | ||
786 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818" | ||
787 | integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== | ||
788 | |||
908 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.4.2: | 789 | addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.4.2: |
909 | version "1.5.1" | 790 | version "1.5.1" |
910 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.1.tgz#bfada13fd6aeeeac19f1e9f7d84b4bbab45e5208" | 791 | resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.1.tgz#bfada13fd6aeeeac19f1e9f7d84b4bbab45e5208" |
@@ -953,10 +834,10 @@ ajv-keywords@^3.1.0: | |||
953 | resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" | 834 | resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" |
954 | integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= | 835 | integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= |
955 | 836 | ||
956 | ajv@6.5.3: | 837 | ajv@6.7.0: |
957 | version "6.5.3" | 838 | version "6.7.0" |
958 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9" | 839 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96" |
959 | integrity sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg== | 840 | integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg== |
960 | dependencies: | 841 | dependencies: |
961 | fast-deep-equal "^2.0.1" | 842 | fast-deep-equal "^2.0.1" |
962 | fast-json-stable-stringify "^2.0.0" | 843 | fast-json-stable-stringify "^2.0.0" |
@@ -1029,6 +910,11 @@ ansi-regex@^3.0.0: | |||
1029 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" | 910 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" |
1030 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= | 911 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= |
1031 | 912 | ||
913 | ansi-regex@^4.0.0: | ||
914 | version "4.0.0" | ||
915 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" | ||
916 | integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== | ||
917 | |||
1032 | ansi-styles@^2.2.1: | 918 | ansi-styles@^2.2.1: |
1033 | version "2.2.1" | 919 | version "2.2.1" |
1034 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" | 920 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" |
@@ -1238,7 +1124,7 @@ async-limiter@~1.0.0: | |||
1238 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" | 1124 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" |
1239 | integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== | 1125 | integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== |
1240 | 1126 | ||
1241 | async@1.x, async@^1.5.2: | 1127 | async@^1.5.2: |
1242 | version "1.5.2" | 1128 | version "1.5.2" |
1243 | resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" | 1129 | resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" |
1244 | integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= | 1130 | integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= |
@@ -1260,16 +1146,16 @@ atob@^2.1.1: | |||
1260 | resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" | 1146 | resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" |
1261 | integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== | 1147 | integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== |
1262 | 1148 | ||
1263 | autoprefixer@9.3.1: | 1149 | autoprefixer@9.4.6: |
1264 | version "9.3.1" | 1150 | version "9.4.6" |
1265 | resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.3.1.tgz#71b622174de2b783d5fd99f9ad617b7a3c78443e" | 1151 | resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.6.tgz#0ace275e33b37de16b09a5547dbfe73a98c1d446" |
1266 | integrity sha512-DY9gOh8z3tnCbJ13JIWaeQsoYncTGdsrgCceBaQSIL4nvdrLxgbRSBPevg2XbX7u4QCSfLheSJEEIUUSlkbx6Q== | 1152 | integrity sha512-Yp51mevbOEdxDUy5WjiKtpQaecqYq9OqZSL04rSoCiry7Tc5I9FEyo3bfxiTJc1DfHeKwSFCUYbBAiOQ2VGfiw== |
1267 | dependencies: | 1153 | dependencies: |
1268 | browserslist "^4.3.3" | 1154 | browserslist "^4.4.1" |
1269 | caniuse-lite "^1.0.30000898" | 1155 | caniuse-lite "^1.0.30000929" |
1270 | normalize-range "^0.1.2" | 1156 | normalize-range "^0.1.2" |
1271 | num2fraction "^1.2.2" | 1157 | num2fraction "^1.2.2" |
1272 | postcss "^7.0.5" | 1158 | postcss "^7.0.13" |
1273 | postcss-value-parser "^3.3.1" | 1159 | postcss-value-parser "^3.3.1" |
1274 | 1160 | ||
1275 | awesome-typescript-loader@5.2.1: | 1161 | awesome-typescript-loader@5.2.1: |
@@ -1536,6 +1422,11 @@ big.js@^3.1.3: | |||
1536 | resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" | 1422 | resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" |
1537 | integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== | 1423 | integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== |
1538 | 1424 | ||
1425 | big.js@^5.2.2: | ||
1426 | version "5.2.2" | ||
1427 | resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" | ||
1428 | integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== | ||
1429 | |||
1539 | binary-extensions@^1.0.0: | 1430 | binary-extensions@^1.0.0: |
1540 | version "1.12.0" | 1431 | version "1.12.0" |
1541 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" | 1432 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" |
@@ -1588,7 +1479,7 @@ bittorrent-protocol@^3.0.0: | |||
1588 | unordered-array-remove "^1.0.2" | 1479 | unordered-array-remove "^1.0.2" |
1589 | xtend "^4.0.0" | 1480 | xtend "^4.0.0" |
1590 | 1481 | ||
1591 | bittorrent-tracker@^9.0.0: | 1482 | bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.10.1: |
1592 | version "9.10.1" | 1483 | version "9.10.1" |
1593 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11" | 1484 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11" |
1594 | integrity sha512-n5zTL/g6Wt0rb2EnkiyiaGYhth7I/N0/xMqGUpvGX/7g1scDGBVPhJnXR8lfp3/OMj681fv40o4q/otECMtZSA== | 1485 | integrity sha512-n5zTL/g6Wt0rb2EnkiyiaGYhth7I/N0/xMqGUpvGX/7g1scDGBVPhJnXR8lfp3/OMj681fv40o4q/otECMtZSA== |
@@ -1653,7 +1544,7 @@ blocking-proxy@^1.0.0: | |||
1653 | dependencies: | 1544 | dependencies: |
1654 | minimist "^1.2.0" | 1545 | minimist "^1.2.0" |
1655 | 1546 | ||
1656 | bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.2: | 1547 | bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3: |
1657 | version "3.5.3" | 1548 | version "3.5.3" |
1658 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" | 1549 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" |
1659 | integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== | 1550 | integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== |
@@ -1822,14 +1713,14 @@ browserify-zlib@^0.2.0: | |||
1822 | dependencies: | 1713 | dependencies: |
1823 | pako "~1.0.5" | 1714 | pako "~1.0.5" |
1824 | 1715 | ||
1825 | browserslist@^4.3.3: | 1716 | browserslist@^4.4.1: |
1826 | version "4.3.5" | 1717 | version "4.4.1" |
1827 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.5.tgz#1a917678acc07b55606748ea1adf9846ea8920f7" | 1718 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" |
1828 | integrity sha512-z9ZhGc3d9e/sJ9dIx5NFXkKoaiQTnrvrMsN3R1fGb1tkWWNSz12UewJn9TNxGo1l7J23h0MRaPmk7jfeTZYs1w== | 1719 | integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== |
1829 | dependencies: | 1720 | dependencies: |
1830 | caniuse-lite "^1.0.30000912" | 1721 | caniuse-lite "^1.0.30000929" |
1831 | electron-to-chromium "^1.3.86" | 1722 | electron-to-chromium "^1.3.103" |
1832 | node-releases "^1.0.5" | 1723 | node-releases "^1.1.3" |
1833 | 1724 | ||
1834 | browserstack@^1.5.1: | 1725 | browserstack@^1.5.1: |
1835 | version "1.5.1" | 1726 | version "1.5.1" |
@@ -1953,7 +1844,7 @@ cacache@^10.0.4: | |||
1953 | unique-filename "^1.1.0" | 1844 | unique-filename "^1.1.0" |
1954 | y18n "^4.0.0" | 1845 | y18n "^4.0.0" |
1955 | 1846 | ||
1956 | cacache@^11.0.1, cacache@^11.0.2, cacache@^11.2.0: | 1847 | cacache@^11.0.1, cacache@^11.0.2: |
1957 | version "11.3.1" | 1848 | version "11.3.1" |
1958 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.1.tgz#d09d25f6c4aca7a6d305d141ae332613aa1d515f" | 1849 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.1.tgz#d09d25f6c4aca7a6d305d141ae332613aa1d515f" |
1959 | integrity sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA== | 1850 | integrity sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA== |
@@ -1973,6 +1864,26 @@ cacache@^11.0.1, cacache@^11.0.2, cacache@^11.2.0: | |||
1973 | unique-filename "^1.1.0" | 1864 | unique-filename "^1.1.0" |
1974 | y18n "^4.0.0" | 1865 | y18n "^4.0.0" |
1975 | 1866 | ||
1867 | cacache@^11.3.2: | ||
1868 | version "11.3.2" | ||
1869 | resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" | ||
1870 | integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg== | ||
1871 | dependencies: | ||
1872 | bluebird "^3.5.3" | ||
1873 | chownr "^1.1.1" | ||
1874 | figgy-pudding "^3.5.1" | ||
1875 | glob "^7.1.3" | ||
1876 | graceful-fs "^4.1.15" | ||
1877 | lru-cache "^5.1.1" | ||
1878 | mississippi "^3.0.0" | ||
1879 | mkdirp "^0.5.1" | ||
1880 | move-concurrently "^1.0.1" | ||
1881 | promise-inflight "^1.0.1" | ||
1882 | rimraf "^2.6.2" | ||
1883 | ssri "^6.0.1" | ||
1884 | unique-filename "^1.1.1" | ||
1885 | y18n "^4.0.0" | ||
1886 | |||
1976 | cache-base@^1.0.1: | 1887 | cache-base@^1.0.1: |
1977 | version "1.0.1" | 1888 | version "1.0.1" |
1978 | resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" | 1889 | resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" |
@@ -2041,10 +1952,10 @@ camelcase@^5.0.0: | |||
2041 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" | 1952 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" |
2042 | integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== | 1953 | integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== |
2043 | 1954 | ||
2044 | caniuse-lite@^1.0.30000898, caniuse-lite@^1.0.30000912: | 1955 | caniuse-lite@^1.0.30000929: |
2045 | version "1.0.30000914" | 1956 | version "1.0.30000935" |
2046 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000914.tgz#f802b4667c24d0255f54a95818dcf8e1aa41f624" | 1957 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000935.tgz#d1b59df00b46f4921bb84a8a34c1d172b346df59" |
2047 | integrity sha512-qqj0CL1xANgg6iDOybiPTIxtsmAnfIky9mBC35qgWrnK4WwmhqfpmkDYMYgwXJ8LRZ3/2jXlCntulO8mBaAgSg== | 1958 | integrity sha512-1Y2uJ5y56qDt3jsDTdBHL1OqiImzjoQcBG6Yl3Qizq8mcc2SgCFpi+ZwLLqkztYnk9l87IYqRlNBnPSOTbFkXQ== |
2048 | 1959 | ||
2049 | canonical-path@1.0.0: | 1960 | canonical-path@1.0.0: |
2050 | version "1.0.0" | 1961 | version "1.0.0" |
@@ -2083,6 +1994,15 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1: | |||
2083 | escape-string-regexp "^1.0.5" | 1994 | escape-string-regexp "^1.0.5" |
2084 | supports-color "^5.3.0" | 1995 | supports-color "^5.3.0" |
2085 | 1996 | ||
1997 | chalk@^2.4.2: | ||
1998 | version "2.4.2" | ||
1999 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" | ||
2000 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== | ||
2001 | dependencies: | ||
2002 | ansi-styles "^3.2.1" | ||
2003 | escape-string-regexp "^1.0.5" | ||
2004 | supports-color "^5.3.0" | ||
2005 | |||
2086 | chardet@^0.7.0: | 2006 | chardet@^0.7.0: |
2087 | version "0.7.0" | 2007 | version "0.7.0" |
2088 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" | 2008 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" |
@@ -2320,11 +2240,6 @@ commander@^2.12.1, commander@^2.18.0, commander@^2.9.0: | |||
2320 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" | 2240 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" |
2321 | integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== | 2241 | integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== |
2322 | 2242 | ||
2323 | commander@~2.13.0: | ||
2324 | version "2.13.0" | ||
2325 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" | ||
2326 | integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== | ||
2327 | |||
2328 | commondir@^1.0.1: | 2243 | commondir@^1.0.1: |
2329 | version "1.0.1" | 2244 | version "1.0.1" |
2330 | resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" | 2245 | resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" |
@@ -2473,10 +2388,10 @@ copy-descriptor@^0.1.0: | |||
2473 | resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" | 2388 | resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" |
2474 | integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= | 2389 | integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= |
2475 | 2390 | ||
2476 | copy-webpack-plugin@4.5.4: | 2391 | copy-webpack-plugin@4.6.0: |
2477 | version "4.5.4" | 2392 | version "4.6.0" |
2478 | resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.4.tgz#f2b2782b3cd5225535c3dc166a80067e7d940f27" | 2393 | resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae" |
2479 | integrity sha512-0lstlEyj74OAtYMrDxlNZsU7cwFijAI3Ofz2fD6Mpo9r4xCv4yegfa3uHIKvZY1NSuOtE9nvG6TAhJ+uz9gDaQ== | 2394 | integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA== |
2480 | dependencies: | 2395 | dependencies: |
2481 | cacache "^10.0.4" | 2396 | cacache "^10.0.4" |
2482 | find-cache-dir "^1.0.0" | 2397 | find-cache-dir "^1.0.0" |
@@ -2774,6 +2689,13 @@ debug@^3.1.0, debug@^3.2.5: | |||
2774 | dependencies: | 2689 | dependencies: |
2775 | ms "^2.1.1" | 2690 | ms "^2.1.1" |
2776 | 2691 | ||
2692 | debug@^4.1.1: | ||
2693 | version "4.1.1" | ||
2694 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" | ||
2695 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== | ||
2696 | dependencies: | ||
2697 | ms "^2.1.1" | ||
2698 | |||
2777 | decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: | 2699 | decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: |
2778 | version "1.2.0" | 2700 | version "1.2.0" |
2779 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" | 2701 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" |
@@ -2944,7 +2866,7 @@ detect-newline@^2.1.0: | |||
2944 | resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" | 2866 | resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" |
2945 | integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= | 2867 | integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= |
2946 | 2868 | ||
2947 | detect-node@^2.0.3: | 2869 | detect-node@^2.0.4: |
2948 | version "2.0.4" | 2870 | version "2.0.4" |
2949 | resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" | 2871 | resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" |
2950 | integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== | 2872 | integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== |
@@ -3141,10 +3063,10 @@ ejs@^2.6.1: | |||
3141 | resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" | 3063 | resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" |
3142 | integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== | 3064 | integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== |
3143 | 3065 | ||
3144 | electron-to-chromium@^1.3.86: | 3066 | electron-to-chromium@^1.3.103: |
3145 | version "1.3.88" | 3067 | version "1.3.113" |
3146 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.88.tgz#f36ab32634f49ef2b0fdc1e82e2d1cc17feb29e7" | 3068 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" |
3147 | integrity sha512-UPV4NuQMKeUh1S0OWRvwg0PI8ASHN9kBC8yDTk1ROXLC85W5GnhTRu/MZu3Teqx3JjlQYuckuHYXSUSgtb3J+A== | 3069 | integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== |
3148 | 3070 | ||
3149 | elliptic@^6.0.0: | 3071 | elliptic@^6.0.0: |
3150 | version "6.4.1" | 3072 | version "6.4.1" |
@@ -3359,18 +3281,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: | |||
3359 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" | 3281 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" |
3360 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= | 3282 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= |
3361 | 3283 | ||
3362 | escodegen@1.8.x: | ||
3363 | version "1.8.1" | ||
3364 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" | ||
3365 | integrity sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg= | ||
3366 | dependencies: | ||
3367 | esprima "^2.7.1" | ||
3368 | estraverse "^1.9.1" | ||
3369 | esutils "^2.0.2" | ||
3370 | optionator "^0.8.1" | ||
3371 | optionalDependencies: | ||
3372 | source-map "~0.2.0" | ||
3373 | |||
3374 | escodegen@^1.9.1: | 3284 | escodegen@^1.9.1: |
3375 | version "1.11.0" | 3285 | version "1.11.0" |
3376 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" | 3286 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" |
@@ -3391,11 +3301,6 @@ eslint-scope@^4.0.0: | |||
3391 | esrecurse "^4.1.0" | 3301 | esrecurse "^4.1.0" |
3392 | estraverse "^4.1.1" | 3302 | estraverse "^4.1.1" |
3393 | 3303 | ||
3394 | esprima@2.7.x, esprima@^2.7.1: | ||
3395 | version "2.7.3" | ||
3396 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" | ||
3397 | integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= | ||
3398 | |||
3399 | esprima@^3.1.3, esprima@~3.1.0: | 3304 | esprima@^3.1.3, esprima@~3.1.0: |
3400 | version "3.1.3" | 3305 | version "3.1.3" |
3401 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" | 3306 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" |
@@ -3413,11 +3318,6 @@ esrecurse@^4.1.0: | |||
3413 | dependencies: | 3318 | dependencies: |
3414 | estraverse "^4.1.0" | 3319 | estraverse "^4.1.0" |
3415 | 3320 | ||
3416 | estraverse@^1.9.1: | ||
3417 | version "1.9.3" | ||
3418 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" | ||
3419 | integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q= | ||
3420 | |||
3421 | estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: | 3321 | estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: |
3422 | version "4.2.0" | 3322 | version "4.2.0" |
3423 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" | 3323 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" |
@@ -3438,7 +3338,7 @@ etag@~1.8.1: | |||
3438 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" | 3338 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" |
3439 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= | 3339 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= |
3440 | 3340 | ||
3441 | eventemitter3@^3.0.0: | 3341 | eventemitter3@3.1.0, eventemitter3@^3.0.0: |
3442 | version "3.1.0" | 3342 | version "3.1.0" |
3443 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" | 3343 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" |
3444 | integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== | 3344 | integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== |
@@ -3448,6 +3348,11 @@ events@^1.0.0: | |||
3448 | resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" | 3348 | resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" |
3449 | integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= | 3349 | integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= |
3450 | 3350 | ||
3351 | events@^3.0.0: | ||
3352 | version "3.0.0" | ||
3353 | resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" | ||
3354 | integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== | ||
3355 | |||
3451 | eventsource@^1.0.7: | 3356 | eventsource@^1.0.7: |
3452 | version "1.0.7" | 3357 | version "1.0.7" |
3453 | resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" | 3358 | resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" |
@@ -3721,7 +3626,15 @@ figures@^2.0.0: | |||
3721 | dependencies: | 3626 | dependencies: |
3722 | escape-string-regexp "^1.0.5" | 3627 | escape-string-regexp "^1.0.5" |
3723 | 3628 | ||
3724 | file-loader@2.0.0, file-loader@^2.0.0: | 3629 | file-loader@3.0.1: |
3630 | version "3.0.1" | ||
3631 | resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" | ||
3632 | integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== | ||
3633 | dependencies: | ||
3634 | loader-utils "^1.0.2" | ||
3635 | schema-utils "^1.0.0" | ||
3636 | |||
3637 | file-loader@^2.0.0: | ||
3725 | version "2.0.0" | 3638 | version "2.0.0" |
3726 | resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-2.0.0.tgz#39749c82f020b9e85901dcff98e8004e6401cfde" | 3639 | resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-2.0.0.tgz#39749c82f020b9e85901dcff98e8004e6401cfde" |
3727 | integrity sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ== | 3640 | integrity sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ== |
@@ -4043,7 +3956,7 @@ genfun@^5.0.0: | |||
4043 | resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" | 3956 | resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" |
4044 | integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== | 3957 | integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== |
4045 | 3958 | ||
4046 | get-browser-rtc@^1.0.0: | 3959 | get-browser-rtc@^1.0.0, get-browser-rtc@^1.0.2: |
4047 | version "1.0.2" | 3960 | version "1.0.2" |
4048 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9" | 3961 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9" |
4049 | integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk= | 3962 | integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk= |
@@ -4146,17 +4059,6 @@ glob@7.1.3, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glo | |||
4146 | once "^1.3.0" | 4059 | once "^1.3.0" |
4147 | path-is-absolute "^1.0.0" | 4060 | path-is-absolute "^1.0.0" |
4148 | 4061 | ||
4149 | glob@^5.0.15: | ||
4150 | version "5.0.15" | ||
4151 | resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" | ||
4152 | integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= | ||
4153 | dependencies: | ||
4154 | inflight "^1.0.4" | ||
4155 | inherits "2" | ||
4156 | minimatch "2 || 3" | ||
4157 | once "^1.3.0" | ||
4158 | path-is-absolute "^1.0.0" | ||
4159 | |||
4160 | global-modules-path@^2.3.0: | 4062 | global-modules-path@^2.3.0: |
4161 | version "2.3.1" | 4063 | version "2.3.1" |
4162 | resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.1.tgz#e541f4c800a1a8514a990477b267ac67525b9931" | 4064 | resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.1.tgz#e541f4c800a1a8514a990477b267ac67525b9931" |
@@ -4224,7 +4126,7 @@ globule@^1.0.0: | |||
4224 | lodash "~4.17.10" | 4126 | lodash "~4.17.10" |
4225 | minimatch "~3.0.2" | 4127 | minimatch "~3.0.2" |
4226 | 4128 | ||
4227 | graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: | 4129 | graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: |
4228 | version "4.1.15" | 4130 | version "4.1.15" |
4229 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" | 4131 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" |
4230 | integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== | 4132 | integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== |
@@ -4242,12 +4144,12 @@ gzip-size@^5.0.0: | |||
4242 | duplexer "^0.1.1" | 4144 | duplexer "^0.1.1" |
4243 | pify "^3.0.0" | 4145 | pify "^3.0.0" |
4244 | 4146 | ||
4245 | handle-thing@^1.2.5: | 4147 | handle-thing@^2.0.0: |
4246 | version "1.2.5" | 4148 | version "2.0.0" |
4247 | resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" | 4149 | resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" |
4248 | integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ= | 4150 | integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== |
4249 | 4151 | ||
4250 | handlebars@^4.0.1, handlebars@^4.0.11, handlebars@^4.0.3: | 4152 | handlebars@^4.0.11, handlebars@^4.0.3: |
4251 | version "4.0.12" | 4153 | version "4.0.12" |
4252 | resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" | 4154 | resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" |
4253 | integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== | 4155 | integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== |
@@ -4369,6 +4271,14 @@ he@1.2.x: | |||
4369 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" | 4271 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" |
4370 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== | 4272 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== |
4371 | 4273 | ||
4274 | hls.js@^0.12.2: | ||
4275 | version "0.12.2" | ||
4276 | resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-0.12.2.tgz#64a969a78cc25991ed5de19357b1dc3f178ac23b" | ||
4277 | integrity sha512-lQBSXggw9OzEuaUllUBoSxPcf7neFgnEiDRfCdCNdIPtUeV7vXZ0OeASx6EWtZTBiqSSPigoOX1Y+AR5dA1Feg== | ||
4278 | dependencies: | ||
4279 | eventemitter3 "3.1.0" | ||
4280 | url-toolkit "^2.1.6" | ||
4281 | |||
4372 | hmac-drbg@^1.0.0: | 4282 | hmac-drbg@^1.0.0: |
4373 | version "1.0.1" | 4283 | version "1.0.1" |
4374 | resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" | 4284 | resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" |
@@ -4701,10 +4611,10 @@ ini@1.3.5, ini@^1.3.4, ini@~1.3.0: | |||
4701 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" | 4611 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" |
4702 | integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== | 4612 | integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== |
4703 | 4613 | ||
4704 | inquirer@6.2.0: | 4614 | inquirer@6.2.1: |
4705 | version "6.2.0" | 4615 | version "6.2.1" |
4706 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" | 4616 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" |
4707 | integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== | 4617 | integrity sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg== |
4708 | dependencies: | 4618 | dependencies: |
4709 | ansi-escapes "^3.0.0" | 4619 | ansi-escapes "^3.0.0" |
4710 | chalk "^2.0.0" | 4620 | chalk "^2.0.0" |
@@ -4717,7 +4627,7 @@ inquirer@6.2.0: | |||
4717 | run-async "^2.2.0" | 4627 | run-async "^2.2.0" |
4718 | rxjs "^6.1.0" | 4628 | rxjs "^6.1.0" |
4719 | string-width "^2.1.0" | 4629 | string-width "^2.1.0" |
4720 | strip-ansi "^4.0.0" | 4630 | strip-ansi "^5.0.0" |
4721 | through "^2.3.6" | 4631 | through "^2.3.6" |
4722 | 4632 | ||
4723 | internal-ip@^3.0.1: | 4633 | internal-ip@^3.0.1: |
@@ -5266,26 +5176,6 @@ istanbul-reports@^2.0.1: | |||
5266 | dependencies: | 5176 | dependencies: |
5267 | handlebars "^4.0.11" | 5177 | handlebars "^4.0.11" |
5268 | 5178 | ||
5269 | istanbul@0.4.5: | ||
5270 | version "0.4.5" | ||
5271 | resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" | ||
5272 | integrity sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs= | ||
5273 | dependencies: | ||
5274 | abbrev "1.0.x" | ||
5275 | async "1.x" | ||
5276 | escodegen "1.8.x" | ||
5277 | esprima "2.7.x" | ||
5278 | glob "^5.0.15" | ||
5279 | handlebars "^4.0.1" | ||
5280 | js-yaml "3.x" | ||
5281 | mkdirp "0.5.x" | ||
5282 | nopt "3.x" | ||
5283 | once "1.x" | ||
5284 | resolve "1.1.x" | ||
5285 | supports-color "^3.1.0" | ||
5286 | which "^1.1.1" | ||
5287 | wordwrap "^1.0.0" | ||
5288 | |||
5289 | jasmine-core@^3.1.0: | 5179 | jasmine-core@^3.1.0: |
5290 | version "3.3.0" | 5180 | version "3.3.0" |
5291 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e" | 5181 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e" |
@@ -5668,7 +5558,7 @@ js-tokens@^3.0.2: | |||
5668 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" | 5558 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" |
5669 | integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= | 5559 | integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= |
5670 | 5560 | ||
5671 | js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: | 5561 | js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: |
5672 | version "3.12.0" | 5562 | version "3.12.0" |
5673 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" | 5563 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" |
5674 | integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== | 5564 | integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== |
@@ -5782,6 +5672,13 @@ json5@^0.5.0, json5@^0.5.1: | |||
5782 | resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" | 5672 | resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" |
5783 | integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= | 5673 | integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= |
5784 | 5674 | ||
5675 | json5@^1.0.1: | ||
5676 | version "1.0.1" | ||
5677 | resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" | ||
5678 | integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== | ||
5679 | dependencies: | ||
5680 | minimist "^1.2.0" | ||
5681 | |||
5785 | jsonfile@^4.0.0: | 5682 | jsonfile@^4.0.0: |
5786 | version "4.0.0" | 5683 | version "4.0.0" |
5787 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" | 5684 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" |
@@ -5994,10 +5891,10 @@ less-loader@4.1.0: | |||
5994 | loader-utils "^1.1.0" | 5891 | loader-utils "^1.1.0" |
5995 | pify "^3.0.0" | 5892 | pify "^3.0.0" |
5996 | 5893 | ||
5997 | less@3.8.1: | 5894 | less@3.9.0: |
5998 | version "3.8.1" | 5895 | version "3.9.0" |
5999 | resolved "https://registry.yarnpkg.com/less/-/less-3.8.1.tgz#f31758598ef5a1930dd4caefa9e4340641e71e1d" | 5896 | resolved "https://registry.yarnpkg.com/less/-/less-3.9.0.tgz#b7511c43f37cf57dc87dffd9883ec121289b1474" |
6000 | integrity sha512-8HFGuWmL3FhQR0aH89escFNBQH/nEiYPP2ltDFdQw2chE28Yx2E3lhAIq9Y2saYwLSwa699s4dBVEfCY8Drf7Q== | 5897 | integrity sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w== |
6001 | dependencies: | 5898 | dependencies: |
6002 | clone "^2.1.2" | 5899 | clone "^2.1.2" |
6003 | optionalDependencies: | 5900 | optionalDependencies: |
@@ -6023,11 +5920,12 @@ levn@~0.3.0: | |||
6023 | prelude-ls "~1.1.2" | 5920 | prelude-ls "~1.1.2" |
6024 | type-check "~0.3.2" | 5921 | type-check "~0.3.2" |
6025 | 5922 | ||
6026 | license-webpack-plugin@2.0.2: | 5923 | license-webpack-plugin@2.1.0: |
6027 | version "2.0.2" | 5924 | version "2.1.0" |
6028 | resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.0.2.tgz#9d34b521cb7fca8527945310b05be6ef0248b687" | 5925 | resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.1.0.tgz#83acaa6e89c3c5316effdd80cb4ec9c5cd8efc2f" |
6029 | integrity sha512-GsomZw5VoT20ST8qH2tOjBgbyhn6Pgs9M94g0mbvfBIV1VXufm1iKY+4dbgfTObj1Mp6nSRE3Zf74deOZr0KwA== | 5926 | integrity sha512-vDiBeMWxjE9n6TabQ9J4FH8urFdsRK0Nvxn1cit9biCiR9aq1zBR0X2BlAkEiIG6qPamLeU0GzvIgLkrFc398A== |
6030 | dependencies: | 5927 | dependencies: |
5928 | "@types/webpack-sources" "^0.1.5" | ||
6031 | webpack-sources "^1.2.0" | 5929 | webpack-sources "^1.2.0" |
6032 | 5930 | ||
6033 | lie@~3.1.0: | 5931 | lie@~3.1.0: |
@@ -6090,14 +5988,14 @@ loader-runner@^2.3.0: | |||
6090 | resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979" | 5988 | resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979" |
6091 | integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== | 5989 | integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== |
6092 | 5990 | ||
6093 | loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0: | 5991 | loader-utils@1.2.3: |
6094 | version "1.1.0" | 5992 | version "1.2.3" |
6095 | resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" | 5993 | resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" |
6096 | integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= | 5994 | integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== |
6097 | dependencies: | 5995 | dependencies: |
6098 | big.js "^3.1.3" | 5996 | big.js "^5.2.2" |
6099 | emojis-list "^2.0.0" | 5997 | emojis-list "^2.0.0" |
6100 | json5 "^0.5.0" | 5998 | json5 "^1.0.1" |
6101 | 5999 | ||
6102 | loader-utils@^0.2.16: | 6000 | loader-utils@^0.2.16: |
6103 | version "0.2.17" | 6001 | version "0.2.17" |
@@ -6109,6 +6007,15 @@ loader-utils@^0.2.16: | |||
6109 | json5 "^0.5.0" | 6007 | json5 "^0.5.0" |
6110 | object-assign "^4.0.1" | 6008 | object-assign "^4.0.1" |
6111 | 6009 | ||
6010 | loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0: | ||
6011 | version "1.1.0" | ||
6012 | resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" | ||
6013 | integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= | ||
6014 | dependencies: | ||
6015 | big.js "^3.1.3" | ||
6016 | emojis-list "^2.0.0" | ||
6017 | json5 "^0.5.0" | ||
6018 | |||
6112 | locate-path@^2.0.0: | 6019 | locate-path@^2.0.0: |
6113 | version "2.0.0" | 6020 | version "2.0.0" |
6114 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" | 6021 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" |
@@ -6239,6 +6146,13 @@ lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache | |||
6239 | pseudomap "^1.0.2" | 6146 | pseudomap "^1.0.2" |
6240 | yallist "^2.1.2" | 6147 | yallist "^2.1.2" |
6241 | 6148 | ||
6149 | lru-cache@^5.1.1: | ||
6150 | version "5.1.1" | ||
6151 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" | ||
6152 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== | ||
6153 | dependencies: | ||
6154 | yallist "^3.0.2" | ||
6155 | |||
6242 | lru@^3.0.0, lru@^3.1.0: | 6156 | lru@^3.0.0, lru@^3.1.0: |
6243 | version "3.1.0" | 6157 | version "3.1.0" |
6244 | resolved "https://registry.yarnpkg.com/lru/-/lru-3.1.0.tgz#ea7fb8546d83733396a13091d76cfeb4c06837d5" | 6158 | resolved "https://registry.yarnpkg.com/lru/-/lru-3.1.0.tgz#ea7fb8546d83733396a13091d76cfeb4c06837d5" |
@@ -6251,6 +6165,13 @@ m3u8-parser@4.2.0: | |||
6251 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.2.0.tgz#c8e0785fd17f741f4408b49466889274a9e36447" | 6165 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.2.0.tgz#c8e0785fd17f741f4408b49466889274a9e36447" |
6252 | integrity sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg== | 6166 | integrity sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg== |
6253 | 6167 | ||
6168 | m3u8-parser@^4.3.0: | ||
6169 | version "4.3.0" | ||
6170 | resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.3.0.tgz#4b4e988f87b6d8b2401d209a1d17798285a9da04" | ||
6171 | integrity sha512-bVbjuBMoVIgFL1vpXVIxjeaoB5TPDJRb0m5qiTdM738SGqv/LAmsnVVPlKjM4fulm/rr1XZsKM+owHm+zvqxYA== | ||
6172 | dependencies: | ||
6173 | global "^4.3.2" | ||
6174 | |||
6254 | magic-string@^0.25.0: | 6175 | magic-string@^0.25.0: |
6255 | version "0.25.1" | 6176 | version "0.25.1" |
6256 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" | 6177 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" |
@@ -6532,10 +6453,10 @@ min-document@^2.19.0: | |||
6532 | dependencies: | 6453 | dependencies: |
6533 | dom-walk "^0.1.0" | 6454 | dom-walk "^0.1.0" |
6534 | 6455 | ||
6535 | mini-css-extract-plugin@0.4.4: | 6456 | mini-css-extract-plugin@0.5.0: |
6536 | version "0.4.4" | 6457 | version "0.5.0" |
6537 | resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.4.tgz#c10410a004951bd3cedac1da69053940fccb625d" | 6458 | resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" |
6538 | integrity sha512-o+Jm+ocb0asEngdM6FsZWtZsRzA8koFUudIDwYUfl94M3PejPHG7Vopw5hN9V8WsMkSFpm3tZP3Fesz89EyrfQ== | 6459 | integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw== |
6539 | dependencies: | 6460 | dependencies: |
6540 | loader-utils "^1.1.0" | 6461 | loader-utils "^1.1.0" |
6541 | schema-utils "^1.0.0" | 6462 | schema-utils "^1.0.0" |
@@ -6551,7 +6472,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: | |||
6551 | resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" | 6472 | resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" |
6552 | integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= | 6473 | integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= |
6553 | 6474 | ||
6554 | "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: | 6475 | minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: |
6555 | version "3.0.4" | 6476 | version "3.0.4" |
6556 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" | 6477 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" |
6557 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== | 6478 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== |
@@ -6943,14 +6864,39 @@ node-pre-gyp@^0.10.0: | |||
6943 | semver "^5.3.0" | 6864 | semver "^5.3.0" |
6944 | tar "^4" | 6865 | tar "^4" |
6945 | 6866 | ||
6946 | node-releases@^1.0.5: | 6867 | node-releases@^1.1.3: |
6947 | version "1.0.5" | 6868 | version "1.1.7" |
6948 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.5.tgz#a641adcc968b039a27345d92ef10b093e5cbd41d" | 6869 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.7.tgz#b09a10394d0ed8f7778f72bb861dde68b146303b" |
6949 | integrity sha512-Ky7q0BO1BBkG/rQz6PkEZ59rwo+aSfhczHP1wwq8IowoVdN/FpiP7qp0XW0P2+BVCWe5fQUBozdbVd54q1RbCQ== | 6870 | integrity sha512-bKdrwaqJUPHqlCzDD7so/R+Nk0jGv9a11ZhLrD9f6i947qGLrGAhU3OxRENa19QQmwzGy/g6zCDEuLGDO8HPvA== |
6950 | dependencies: | 6871 | dependencies: |
6951 | semver "^5.3.0" | 6872 | semver "^5.3.0" |
6952 | 6873 | ||
6953 | node-sass@4.10.0, node-sass@^4.9.3: | 6874 | node-sass@4.11.0: |
6875 | version "4.11.0" | ||
6876 | resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" | ||
6877 | integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== | ||
6878 | dependencies: | ||
6879 | async-foreach "^0.1.3" | ||
6880 | chalk "^1.1.1" | ||
6881 | cross-spawn "^3.0.0" | ||
6882 | gaze "^1.0.0" | ||
6883 | get-stdin "^4.0.1" | ||
6884 | glob "^7.0.3" | ||
6885 | in-publish "^2.0.0" | ||
6886 | lodash.assign "^4.2.0" | ||
6887 | lodash.clonedeep "^4.3.2" | ||
6888 | lodash.mergewith "^4.6.0" | ||
6889 | meow "^3.7.0" | ||
6890 | mkdirp "^0.5.1" | ||
6891 | nan "^2.10.0" | ||
6892 | node-gyp "^3.8.0" | ||
6893 | npmlog "^4.0.0" | ||
6894 | request "^2.88.0" | ||
6895 | sass-graph "^2.2.4" | ||
6896 | stdout-stream "^1.4.0" | ||
6897 | "true-case-path" "^1.0.2" | ||
6898 | |||
6899 | node-sass@^4.9.3: | ||
6954 | version "4.10.0" | 6900 | version "4.10.0" |
6955 | resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.10.0.tgz#dcc2b364c0913630945ccbf7a2bbf1f926effca4" | 6901 | resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.10.0.tgz#dcc2b364c0913630945ccbf7a2bbf1f926effca4" |
6956 | integrity sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q== | 6902 | integrity sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q== |
@@ -6975,7 +6921,7 @@ node-sass@4.10.0, node-sass@^4.9.3: | |||
6975 | stdout-stream "^1.4.0" | 6921 | stdout-stream "^1.4.0" |
6976 | "true-case-path" "^1.0.2" | 6922 | "true-case-path" "^1.0.2" |
6977 | 6923 | ||
6978 | "nopt@2 || 3", nopt@3.x: | 6924 | "nopt@2 || 3": |
6979 | version "3.0.6" | 6925 | version "3.0.6" |
6980 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" | 6926 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" |
6981 | integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= | 6927 | integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= |
@@ -7022,7 +6968,7 @@ npm-font-source-sans-pro@^1.0.2: | |||
7022 | resolved "https://registry.yarnpkg.com/npm-font-source-sans-pro/-/npm-font-source-sans-pro-1.0.2.tgz#c55c8ae368eebdbcaca65425a0d7e1f9a192a03e" | 6968 | resolved "https://registry.yarnpkg.com/npm-font-source-sans-pro/-/npm-font-source-sans-pro-1.0.2.tgz#c55c8ae368eebdbcaca65425a0d7e1f9a192a03e" |
7023 | integrity sha1-xVyK42juvbysplQloNfh+aGSoD4= | 6969 | integrity sha1-xVyK42juvbysplQloNfh+aGSoD4= |
7024 | 6970 | ||
7025 | npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: | 6971 | npm-package-arg@6.1.0, npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: |
7026 | version "6.1.0" | 6972 | version "6.1.0" |
7027 | resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" | 6973 | resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" |
7028 | integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA== | 6974 | integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA== |
@@ -7040,7 +6986,7 @@ npm-packlist@^1.1.12, npm-packlist@^1.1.6: | |||
7040 | ignore-walk "^3.0.1" | 6986 | ignore-walk "^3.0.1" |
7041 | npm-bundled "^1.0.1" | 6987 | npm-bundled "^1.0.1" |
7042 | 6988 | ||
7043 | npm-pick-manifest@^2.1.0: | 6989 | npm-pick-manifest@^2.2.3: |
7044 | version "2.2.3" | 6990 | version "2.2.3" |
7045 | resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40" | 6991 | resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40" |
7046 | integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA== | 6992 | integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA== |
@@ -7174,7 +7120,7 @@ object.pick@^1.3.0: | |||
7174 | dependencies: | 7120 | dependencies: |
7175 | isobject "^3.0.1" | 7121 | isobject "^3.0.1" |
7176 | 7122 | ||
7177 | obuf@^1.0.0, obuf@^1.1.1: | 7123 | obuf@^1.0.0, obuf@^1.1.2: |
7178 | version "1.1.2" | 7124 | version "1.1.2" |
7179 | resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" | 7125 | resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" |
7180 | integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== | 7126 | integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== |
@@ -7191,7 +7137,7 @@ on-headers@~1.0.1: | |||
7191 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" | 7137 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" |
7192 | integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= | 7138 | integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= |
7193 | 7139 | ||
7194 | once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: | 7140 | once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: |
7195 | version "1.4.0" | 7141 | version "1.4.0" |
7196 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" | 7142 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" |
7197 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= | 7143 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= |
@@ -7210,14 +7156,7 @@ opener@^1.5.1: | |||
7210 | resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" | 7156 | resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" |
7211 | integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== | 7157 | integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== |
7212 | 7158 | ||
7213 | opn@5.3.0: | 7159 | opn@5.4.0, opn@^5.1.0: |
7214 | version "5.3.0" | ||
7215 | resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" | ||
7216 | integrity sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g== | ||
7217 | dependencies: | ||
7218 | is-wsl "^1.1.0" | ||
7219 | |||
7220 | opn@^5.1.0: | ||
7221 | version "5.4.0" | 7160 | version "5.4.0" |
7222 | resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" | 7161 | resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" |
7223 | integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== | 7162 | integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== |
@@ -7357,6 +7296,26 @@ p-try@^2.0.0: | |||
7357 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" | 7296 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" |
7358 | integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== | 7297 | integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== |
7359 | 7298 | ||
7299 | p2p-media-loader-core@^0.4.0: | ||
7300 | version "0.4.0" | ||
7301 | resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.4.0.tgz#767d56785545bc9c0d8c1a04eb7b67a33e40d0c8" | ||
7302 | integrity sha512-llcFqEDs19o916g2OSIPHPjZweO5caHUm/7P18Qu+qb3swYQYSPNwMLoHnpXROHiH5I+00K8w5enz31oUwiCgA== | ||
7303 | dependencies: | ||
7304 | bittorrent-tracker "^9.10.1" | ||
7305 | debug "^4.1.1" | ||
7306 | events "^3.0.0" | ||
7307 | get-browser-rtc "^1.0.2" | ||
7308 | sha.js "^2.4.11" | ||
7309 | |||
7310 | p2p-media-loader-hlsjs@^0.4.0: | ||
7311 | version "0.4.0" | ||
7312 | resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.4.0.tgz#1b90c88580503d4c3d8017c813abe41803b613ed" | ||
7313 | integrity sha512-IWRs/aGasKD//+dtQkYWAjD/cQx3LMaLkMn0EzLhLpeBj4SLNjlbwOPlbx36M4i39X04Y3WZe9YUeIciId3G5Q== | ||
7314 | dependencies: | ||
7315 | events "^3.0.0" | ||
7316 | m3u8-parser "^4.3.0" | ||
7317 | p2p-media-loader-core "^0.4.0" | ||
7318 | |||
7360 | package-json-versionify@^1.0.2: | 7319 | package-json-versionify@^1.0.2: |
7361 | version "1.0.4" | 7320 | version "1.0.4" |
7362 | resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17" | 7321 | resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17" |
@@ -7364,17 +7323,17 @@ package-json-versionify@^1.0.2: | |||
7364 | dependencies: | 7323 | dependencies: |
7365 | browserify-package-json "^1.0.0" | 7324 | browserify-package-json "^1.0.0" |
7366 | 7325 | ||
7367 | pacote@9.1.1: | 7326 | pacote@9.4.0: |
7368 | version "9.1.1" | 7327 | version "9.4.0" |
7369 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.1.1.tgz#25091f75a25021de8be8d34cc6408728fca3579b" | 7328 | resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.4.0.tgz#af979abdeb175cd347c3e33be3241af1ed254807" |
7370 | integrity sha512-f28Rq5ozzKAA9YwIKw61/ipwAatUZseYmVssDbHHaexF0wRIVotapVEZPAjOT7Eu3LYVqEp0NVpNizoAnYBUaA== | 7329 | integrity sha512-WQ1KL/phGMkedYEQx9ODsjj7xvwLSpdFJJdEXrLyw5SILMxcTNt5DTxT2Z93fXuLFYJBlZJdnwdalrQdB/rX5w== |
7371 | dependencies: | 7330 | dependencies: |
7372 | bluebird "^3.5.2" | 7331 | bluebird "^3.5.3" |
7373 | cacache "^11.2.0" | 7332 | cacache "^11.3.2" |
7374 | figgy-pudding "^3.5.1" | 7333 | figgy-pudding "^3.5.1" |
7375 | get-stream "^4.1.0" | 7334 | get-stream "^4.1.0" |
7376 | glob "^7.1.3" | 7335 | glob "^7.1.3" |
7377 | lru-cache "^4.1.3" | 7336 | lru-cache "^5.1.1" |
7378 | make-fetch-happen "^4.0.1" | 7337 | make-fetch-happen "^4.0.1" |
7379 | minimatch "^3.0.4" | 7338 | minimatch "^3.0.4" |
7380 | minipass "^2.3.5" | 7339 | minipass "^2.3.5" |
@@ -7383,7 +7342,7 @@ pacote@9.1.1: | |||
7383 | normalize-package-data "^2.4.0" | 7342 | normalize-package-data "^2.4.0" |
7384 | npm-package-arg "^6.1.0" | 7343 | npm-package-arg "^6.1.0" |
7385 | npm-packlist "^1.1.12" | 7344 | npm-packlist "^1.1.12" |
7386 | npm-pick-manifest "^2.1.0" | 7345 | npm-pick-manifest "^2.2.3" |
7387 | npm-registry-fetch "^3.8.0" | 7346 | npm-registry-fetch "^3.8.0" |
7388 | osenv "^0.1.5" | 7347 | osenv "^0.1.5" |
7389 | promise-inflight "^1.0.1" | 7348 | promise-inflight "^1.0.1" |
@@ -7393,7 +7352,7 @@ pacote@9.1.1: | |||
7393 | safe-buffer "^5.1.2" | 7352 | safe-buffer "^5.1.2" |
7394 | semver "^5.6.0" | 7353 | semver "^5.6.0" |
7395 | ssri "^6.0.1" | 7354 | ssri "^6.0.1" |
7396 | tar "^4.4.6" | 7355 | tar "^4.4.8" |
7397 | unique-filename "^1.1.1" | 7356 | unique-filename "^1.1.1" |
7398 | which "^1.3.1" | 7357 | which "^1.3.1" |
7399 | 7358 | ||
@@ -7658,15 +7617,6 @@ pngjs@^2.3.1: | |||
7658 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-2.3.1.tgz#11d1e12b9cb64d63e30c143a330f4c1f567da85f" | 7617 | resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-2.3.1.tgz#11d1e12b9cb64d63e30c143a330f4c1f567da85f" |
7659 | integrity sha1-EdHhK5y2TWPjDBQ6Mw9MH1Z9qF8= | 7618 | integrity sha1-EdHhK5y2TWPjDBQ6Mw9MH1Z9qF8= |
7660 | 7619 | ||
7661 | portfinder@1.0.17: | ||
7662 | version "1.0.17" | ||
7663 | resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" | ||
7664 | integrity sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ== | ||
7665 | dependencies: | ||
7666 | async "^1.5.2" | ||
7667 | debug "^2.2.0" | ||
7668 | mkdirp "0.5.x" | ||
7669 | |||
7670 | portfinder@^1.0.9: | 7620 | portfinder@^1.0.9: |
7671 | version "1.0.20" | 7621 | version "1.0.20" |
7672 | resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" | 7622 | resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" |
@@ -7681,10 +7631,10 @@ posix-character-classes@^0.1.0: | |||
7681 | resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" | 7631 | resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" |
7682 | integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= | 7632 | integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= |
7683 | 7633 | ||
7684 | postcss-import@12.0.0: | 7634 | postcss-import@12.0.1: |
7685 | version "12.0.0" | 7635 | version "12.0.1" |
7686 | resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.0.tgz#149f96a4ef0b27525c419784be8517ebd17e92c5" | 7636 | resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" |
7687 | integrity sha512-3KqKRZcaZAvxbY8DVLdd81tG5uKzbUQuiWIvy0o0fzEC42bKacqPYFWbfCQyw6L4LWUaqPz/idvIdbhpgQ32eQ== | 7637 | integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== |
7688 | dependencies: | 7638 | dependencies: |
7689 | postcss "^7.0.1" | 7639 | postcss "^7.0.1" |
7690 | postcss-value-parser "^3.2.3" | 7640 | postcss-value-parser "^3.2.3" |
@@ -7745,14 +7695,14 @@ postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0, postcss-value-parser@^ | |||
7745 | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" | 7695 | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" |
7746 | integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== | 7696 | integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== |
7747 | 7697 | ||
7748 | postcss@7.0.5: | 7698 | postcss@7.0.14, postcss@^7.0.13: |
7749 | version "7.0.5" | 7699 | version "7.0.14" |
7750 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" | 7700 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" |
7751 | integrity sha512-HBNpviAUFCKvEh7NZhw1e8MBPivRszIiUnhrJ+sBFVSYSqubrzwX3KG51mYgcRHX8j/cAgZJedONZcm5jTBdgQ== | 7701 | integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== |
7752 | dependencies: | 7702 | dependencies: |
7753 | chalk "^2.4.1" | 7703 | chalk "^2.4.2" |
7754 | source-map "^0.6.1" | 7704 | source-map "^0.6.1" |
7755 | supports-color "^5.5.0" | 7705 | supports-color "^6.1.0" |
7756 | 7706 | ||
7757 | postcss@^6.0.1, postcss@^6.0.23: | 7707 | postcss@^6.0.1, postcss@^6.0.23: |
7758 | version "6.0.23" | 7708 | version "6.0.23" |
@@ -7763,7 +7713,7 @@ postcss@^6.0.1, postcss@^6.0.23: | |||
7763 | source-map "^0.6.1" | 7713 | source-map "^0.6.1" |
7764 | supports-color "^5.4.0" | 7714 | supports-color "^5.4.0" |
7765 | 7715 | ||
7766 | postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.5: | 7716 | postcss@^7.0.0, postcss@^7.0.1: |
7767 | version "7.0.6" | 7717 | version "7.0.6" |
7768 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2" | 7718 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2" |
7769 | integrity sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug== | 7719 | integrity sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug== |
@@ -7953,21 +7903,11 @@ pumpify@^1.3.3: | |||
7953 | inherits "^2.0.3" | 7903 | inherits "^2.0.3" |
7954 | pump "^2.0.0" | 7904 | pump "^2.0.0" |
7955 | 7905 | ||
7956 | punycode@1.3.2: | 7906 | punycode@1.3.2, punycode@^1.2.4, punycode@^1.4.1, punycode@^2.1.0, punycode@^2.1.1: |
7957 | version "1.3.2" | ||
7958 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" | ||
7959 | integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= | ||
7960 | |||
7961 | punycode@^1.2.4, punycode@^1.4.1: | ||
7962 | version "1.4.1" | 7907 | version "1.4.1" |
7963 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" | 7908 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" |
7964 | integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= | 7909 | integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= |
7965 | 7910 | ||
7966 | punycode@^2.1.0, punycode@^2.1.1: | ||
7967 | version "2.1.1" | ||
7968 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" | ||
7969 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== | ||
7970 | |||
7971 | purify-css@^1.2.5: | 7911 | purify-css@^1.2.5: |
7972 | version "1.2.5" | 7912 | version "1.2.5" |
7973 | resolved "https://registry.yarnpkg.com/purify-css/-/purify-css-1.2.5.tgz#c4b9ec90735765f3e247ba6a3b49f132f3482500" | 7913 | resolved "https://registry.yarnpkg.com/purify-css/-/purify-css-1.2.5.tgz#c4b9ec90735765f3e247ba6a3b49f132f3482500" |
@@ -8098,7 +8038,15 @@ raw-body@2.3.3: | |||
8098 | iconv-lite "0.4.23" | 8038 | iconv-lite "0.4.23" |
8099 | unpipe "1.0.0" | 8039 | unpipe "1.0.0" |
8100 | 8040 | ||
8101 | raw-loader@0.5.1, raw-loader@^0.5.1: | 8041 | raw-loader@1.0.0: |
8042 | version "1.0.0" | ||
8043 | resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405" | ||
8044 | integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA== | ||
8045 | dependencies: | ||
8046 | loader-utils "^1.1.0" | ||
8047 | schema-utils "^1.0.0" | ||
8048 | |||
8049 | raw-loader@^0.5.1: | ||
8102 | version "0.5.1" | 8050 | version "0.5.1" |
8103 | resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" | 8051 | resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" |
8104 | integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= | 8052 | integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= |
@@ -8174,7 +8122,7 @@ read-pkg@^2.0.0: | |||
8174 | normalize-package-data "^2.3.2" | 8122 | normalize-package-data "^2.3.2" |
8175 | path-type "^2.0.0" | 8123 | path-type "^2.0.0" |
8176 | 8124 | ||
8177 | "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.2, readable-stream@^2.3.3, readable-stream@^2.3.4, readable-stream@^2.3.6, readable-stream@~2.3.6: | 8125 | "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.3, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.2, readable-stream@^2.3.3, readable-stream@^2.3.4, readable-stream@^2.3.6, readable-stream@~2.3.6: |
8178 | version "2.3.6" | 8126 | version "2.3.6" |
8179 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" | 8127 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" |
8180 | integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== | 8128 | integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== |
@@ -8453,7 +8401,7 @@ resolve-url@^0.2.1: | |||
8453 | resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" | 8401 | resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" |
8454 | integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= | 8402 | integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= |
8455 | 8403 | ||
8456 | resolve@1.1.7, resolve@1.1.x: | 8404 | resolve@1.1.7: |
8457 | version "1.1.7" | 8405 | version "1.1.7" |
8458 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" | 8406 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" |
8459 | integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= | 8407 | integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= |
@@ -8745,16 +8693,11 @@ semver-intersect@1.4.0: | |||
8745 | dependencies: | 8693 | dependencies: |
8746 | semver "^5.0.0" | 8694 | semver "^5.0.0" |
8747 | 8695 | ||
8748 | "semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0: | 8696 | "semver@2 || 3 || 4 || 5", semver@5.6.0, semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0: |
8749 | version "5.6.0" | 8697 | version "5.6.0" |
8750 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" | 8698 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" |
8751 | integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== | 8699 | integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== |
8752 | 8700 | ||
8753 | semver@5.5.1: | ||
8754 | version "5.5.1" | ||
8755 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" | ||
8756 | integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== | ||
8757 | |||
8758 | semver@~5.3.0: | 8701 | semver@~5.3.0: |
8759 | version "5.3.0" | 8702 | version "5.3.0" |
8760 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" | 8703 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" |
@@ -8842,7 +8785,7 @@ setprototypeof@1.1.0: | |||
8842 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" | 8785 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" |
8843 | integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== | 8786 | integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== |
8844 | 8787 | ||
8845 | sha.js@^2.4.0, sha.js@^2.4.8: | 8788 | sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: |
8846 | version "2.4.11" | 8789 | version "2.4.11" |
8847 | resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" | 8790 | resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" |
8848 | integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== | 8791 | integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== |
@@ -9118,10 +9061,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: | |||
9118 | source-map-url "^0.4.0" | 9061 | source-map-url "^0.4.0" |
9119 | urix "^0.1.0" | 9062 | urix "^0.1.0" |
9120 | 9063 | ||
9121 | source-map-support@0.5.9, source-map-support@^0.5.3, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.6: | 9064 | source-map-support@0.5.10, source-map-support@~0.5.9: |
9122 | version "0.5.9" | 9065 | version "0.5.10" |
9123 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" | 9066 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" |
9124 | integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== | 9067 | integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== |
9125 | dependencies: | 9068 | dependencies: |
9126 | buffer-from "^1.0.0" | 9069 | buffer-from "^1.0.0" |
9127 | source-map "^0.6.0" | 9070 | source-map "^0.6.0" |
@@ -9133,6 +9076,14 @@ source-map-support@^0.4.15, source-map-support@~0.4.0: | |||
9133 | dependencies: | 9076 | dependencies: |
9134 | source-map "^0.5.6" | 9077 | source-map "^0.5.6" |
9135 | 9078 | ||
9079 | source-map-support@^0.5.3, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.6: | ||
9080 | version "0.5.9" | ||
9081 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" | ||
9082 | integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== | ||
9083 | dependencies: | ||
9084 | buffer-from "^1.0.0" | ||
9085 | source-map "^0.6.0" | ||
9086 | |||
9136 | source-map-url@^0.4.0: | 9087 | source-map-url@^0.4.0: |
9137 | version "0.4.0" | 9088 | version "0.4.0" |
9138 | resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" | 9089 | resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" |
@@ -9172,13 +9123,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: | |||
9172 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" | 9123 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" |
9173 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== | 9124 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== |
9174 | 9125 | ||
9175 | source-map@~0.2.0: | ||
9176 | version "0.2.0" | ||
9177 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" | ||
9178 | integrity sha1-2rc/vPwrqBm03gO9b26qSBZLP50= | ||
9179 | dependencies: | ||
9180 | amdefine ">=0.0.4" | ||
9181 | |||
9182 | sourcemap-codec@^1.4.1: | 9126 | sourcemap-codec@^1.4.1: |
9183 | version "1.4.4" | 9127 | version "1.4.4" |
9184 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f" | 9128 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f" |
@@ -9210,35 +9154,33 @@ spdx-license-ids@^3.0.0: | |||
9210 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2" | 9154 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2" |
9211 | integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg== | 9155 | integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg== |
9212 | 9156 | ||
9213 | spdy-transport@^2.0.18: | 9157 | spdy-transport@^3.0.0: |
9214 | version "2.1.1" | 9158 | version "3.0.0" |
9215 | resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.1.1.tgz#c54815d73858aadd06ce63001e7d25fa6441623b" | 9159 | resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" |
9216 | integrity sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q== | 9160 | integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== |
9217 | dependencies: | 9161 | dependencies: |
9218 | debug "^2.6.8" | 9162 | debug "^4.1.0" |
9219 | detect-node "^2.0.3" | 9163 | detect-node "^2.0.4" |
9220 | hpack.js "^2.1.6" | 9164 | hpack.js "^2.1.6" |
9221 | obuf "^1.1.1" | 9165 | obuf "^1.1.2" |
9222 | readable-stream "^2.2.9" | 9166 | readable-stream "^3.0.6" |
9223 | safe-buffer "^5.0.1" | 9167 | wbuf "^1.7.3" |
9224 | wbuf "^1.7.2" | ||
9225 | 9168 | ||
9226 | spdy@^3.4.1: | 9169 | spdy@^4.0.0: |
9227 | version "3.4.7" | 9170 | version "4.0.0" |
9228 | resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" | 9171 | resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52" |
9229 | integrity sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw= | 9172 | integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q== |
9230 | dependencies: | 9173 | dependencies: |
9231 | debug "^2.6.8" | 9174 | debug "^4.1.0" |
9232 | handle-thing "^1.2.5" | 9175 | handle-thing "^2.0.0" |
9233 | http-deceiver "^1.2.7" | 9176 | http-deceiver "^1.2.7" |
9234 | safe-buffer "^5.0.1" | ||
9235 | select-hose "^2.0.0" | 9177 | select-hose "^2.0.0" |
9236 | spdy-transport "^2.0.18" | 9178 | spdy-transport "^3.0.0" |
9237 | 9179 | ||
9238 | speed-measure-webpack-plugin@1.2.3: | 9180 | speed-measure-webpack-plugin@1.3.0: |
9239 | version "1.2.3" | 9181 | version "1.3.0" |
9240 | resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.2.3.tgz#de170b5cefbfa1c039d95e639edd3ad50cfc7c48" | 9182 | resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.0.tgz#c7ffafef513df3d63d5d546c8fc1986dfc4969aa" |
9241 | integrity sha512-p+taQ69VkRUXYMoZOx2nxV/Tz8tt79ahctoZJyJDHWP7fEYvwFNf5Pd73k5kZ6auu0pTsPNLEUwWpM8mCk85Zw== | 9183 | integrity sha512-b9Yd0TrzceMVYSbuamM1sFsGM1oVfyFTM22gOoyLhymNvBVApuYpkdFOgYkKJpN/KhTpcCYcTGHg7X+FJ33Vvw== |
9242 | dependencies: | 9184 | dependencies: |
9243 | chalk "^2.0.1" | 9185 | chalk "^2.0.1" |
9244 | 9186 | ||
@@ -9494,6 +9436,13 @@ strip-ansi@^4.0.0: | |||
9494 | dependencies: | 9436 | dependencies: |
9495 | ansi-regex "^3.0.0" | 9437 | ansi-regex "^3.0.0" |
9496 | 9438 | ||
9439 | strip-ansi@^5.0.0: | ||
9440 | version "5.0.0" | ||
9441 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" | ||
9442 | integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== | ||
9443 | dependencies: | ||
9444 | ansi-regex "^4.0.0" | ||
9445 | |||
9497 | strip-bom@3.0.0, strip-bom@^3.0.0: | 9446 | strip-bom@3.0.0, strip-bom@^3.0.0: |
9498 | version "3.0.0" | 9447 | version "3.0.0" |
9499 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" | 9448 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" |
@@ -9557,7 +9506,7 @@ supports-color@^2.0.0: | |||
9557 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" | 9506 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" |
9558 | integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= | 9507 | integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= |
9559 | 9508 | ||
9560 | supports-color@^3.1.0, supports-color@^3.1.2: | 9509 | supports-color@^3.1.2: |
9561 | version "3.2.3" | 9510 | version "3.2.3" |
9562 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" | 9511 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" |
9563 | integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= | 9512 | integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= |
@@ -9571,6 +9520,13 @@ supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co | |||
9571 | dependencies: | 9520 | dependencies: |
9572 | has-flag "^3.0.0" | 9521 | has-flag "^3.0.0" |
9573 | 9522 | ||
9523 | supports-color@^6.1.0: | ||
9524 | version "6.1.0" | ||
9525 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" | ||
9526 | integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== | ||
9527 | dependencies: | ||
9528 | has-flag "^3.0.0" | ||
9529 | |||
9574 | symbol-observable@1.2.0: | 9530 | symbol-observable@1.2.0: |
9575 | version "1.2.0" | 9531 | version "1.2.0" |
9576 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" | 9532 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" |
@@ -9595,7 +9551,7 @@ tar@^2.0.0: | |||
9595 | fstream "^1.0.2" | 9551 | fstream "^1.0.2" |
9596 | inherits "2" | 9552 | inherits "2" |
9597 | 9553 | ||
9598 | tar@^4, tar@^4.4.6: | 9554 | tar@^4, tar@^4.4.8: |
9599 | version "4.4.8" | 9555 | version "4.4.8" |
9600 | resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" | 9556 | resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" |
9601 | integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== | 9557 | integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ== |
@@ -9608,7 +9564,21 @@ tar@^4, tar@^4.4.6: | |||
9608 | safe-buffer "^5.1.2" | 9564 | safe-buffer "^5.1.2" |
9609 | yallist "^3.0.2" | 9565 | yallist "^3.0.2" |
9610 | 9566 | ||
9611 | terser-webpack-plugin@1.1.0, terser-webpack-plugin@^1.1.0: | 9567 | terser-webpack-plugin@1.2.2: |
9568 | version "1.2.2" | ||
9569 | resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9" | ||
9570 | integrity sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg== | ||
9571 | dependencies: | ||
9572 | cacache "^11.0.2" | ||
9573 | find-cache-dir "^2.0.0" | ||
9574 | schema-utils "^1.0.0" | ||
9575 | serialize-javascript "^1.4.0" | ||
9576 | source-map "^0.6.1" | ||
9577 | terser "^3.16.1" | ||
9578 | webpack-sources "^1.1.0" | ||
9579 | worker-farm "^1.5.2" | ||
9580 | |||
9581 | terser-webpack-plugin@^1.1.0: | ||
9612 | version "1.1.0" | 9582 | version "1.1.0" |
9613 | resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528" | 9583 | resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528" |
9614 | integrity sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA== | 9584 | integrity sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA== |
@@ -9622,6 +9592,15 @@ terser-webpack-plugin@1.1.0, terser-webpack-plugin@^1.1.0: | |||
9622 | webpack-sources "^1.1.0" | 9592 | webpack-sources "^1.1.0" |
9623 | worker-farm "^1.5.2" | 9593 | worker-farm "^1.5.2" |
9624 | 9594 | ||
9595 | terser@^3.16.1: | ||
9596 | version "3.16.1" | ||
9597 | resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493" | ||
9598 | integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow== | ||
9599 | dependencies: | ||
9600 | commander "~2.17.1" | ||
9601 | source-map "~0.6.1" | ||
9602 | source-map-support "~0.5.9" | ||
9603 | |||
9625 | terser@^3.8.1: | 9604 | terser@^3.8.1: |
9626 | version "3.11.0" | 9605 | version "3.11.0" |
9627 | resolved "https://registry.yarnpkg.com/terser/-/terser-3.11.0.tgz#60782893e1f4d6788acc696351f40636d0e37af0" | 9606 | resolved "https://registry.yarnpkg.com/terser/-/terser-3.11.0.tgz#60782893e1f4d6788acc696351f40636d0e37af0" |
@@ -9784,10 +9763,10 @@ tr46@^1.0.1: | |||
9784 | dependencies: | 9763 | dependencies: |
9785 | punycode "^2.1.0" | 9764 | punycode "^2.1.0" |
9786 | 9765 | ||
9787 | tree-kill@1.2.0: | 9766 | tree-kill@1.2.1: |
9788 | version "1.2.0" | 9767 | version "1.2.1" |
9789 | resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" | 9768 | resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" |
9790 | integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== | 9769 | integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== |
9791 | 9770 | ||
9792 | trim-newlines@^1.0.0: | 9771 | trim-newlines@^1.0.0: |
9793 | version "1.0.0" | 9772 | version "1.0.0" |
@@ -9953,19 +9932,16 @@ typescript@3.1.6: | |||
9953 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" | 9932 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" |
9954 | integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== | 9933 | integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== |
9955 | 9934 | ||
9935 | typescript@3.2.4: | ||
9936 | version "3.2.4" | ||
9937 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d" | ||
9938 | integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg== | ||
9939 | |||
9956 | uc.micro@^1.0.1, uc.micro@^1.0.5: | 9940 | uc.micro@^1.0.1, uc.micro@^1.0.5: |
9957 | version "1.0.5" | 9941 | version "1.0.5" |
9958 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" | 9942 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" |
9959 | integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== | 9943 | integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== |
9960 | 9944 | ||
9961 | uglify-es@^3.3.4: | ||
9962 | version "3.3.9" | ||
9963 | resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" | ||
9964 | integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ== | ||
9965 | dependencies: | ||
9966 | commander "~2.13.0" | ||
9967 | source-map "~0.6.1" | ||
9968 | |||
9969 | uglify-js@3.4.x, uglify-js@^3.0.6, uglify-js@^3.1.4: | 9945 | uglify-js@3.4.x, uglify-js@^3.0.6, uglify-js@^3.1.4: |
9970 | version "3.4.9" | 9946 | version "3.4.9" |
9971 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" | 9947 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" |
@@ -9974,20 +9950,6 @@ uglify-js@3.4.x, uglify-js@^3.0.6, uglify-js@^3.1.4: | |||
9974 | commander "~2.17.1" | 9950 | commander "~2.17.1" |
9975 | source-map "~0.6.1" | 9951 | source-map "~0.6.1" |
9976 | 9952 | ||
9977 | uglifyjs-webpack-plugin@^1.2.4: | ||
9978 | version "1.3.0" | ||
9979 | resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz#75f548160858163a08643e086d5fefe18a5d67de" | ||
9980 | integrity sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw== | ||
9981 | dependencies: | ||
9982 | cacache "^10.0.4" | ||
9983 | find-cache-dir "^1.0.0" | ||
9984 | schema-utils "^0.4.5" | ||
9985 | serialize-javascript "^1.4.0" | ||
9986 | source-map "^0.6.1" | ||
9987 | uglify-es "^3.3.4" | ||
9988 | webpack-sources "^1.1.0" | ||
9989 | worker-farm "^1.5.2" | ||
9990 | |||
9991 | uint64be@^2.0.2: | 9953 | uint64be@^2.0.2: |
9992 | version "2.0.2" | 9954 | version "2.0.2" |
9993 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" | 9955 | resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5" |
@@ -10082,7 +10044,7 @@ url-parse@^1.4.3: | |||
10082 | querystringify "^2.0.0" | 10044 | querystringify "^2.0.0" |
10083 | requires-port "^1.0.0" | 10045 | requires-port "^1.0.0" |
10084 | 10046 | ||
10085 | url-toolkit@^2.1.1, url-toolkit@^2.1.3: | 10047 | url-toolkit@^2.1.1, url-toolkit@^2.1.3, url-toolkit@^2.1.6: |
10086 | version "2.1.6" | 10048 | version "2.1.6" |
10087 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.1.6.tgz#6d03246499e519aad224c44044a4ae20544154f2" | 10049 | resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.1.6.tgz#6d03246499e519aad224c44044a4ae20544154f2" |
10088 | integrity sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw== | 10050 | integrity sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw== |
@@ -10233,6 +10195,14 @@ videojs-contextmenu-ui@^5.0.0: | |||
10233 | global "^4.3.2" | 10195 | global "^4.3.2" |
10234 | video.js "^6 || ^7" | 10196 | video.js "^6 || ^7" |
10235 | 10197 | ||
10198 | videojs-contrib-quality-levels@^2.0.9: | ||
10199 | version "2.0.9" | ||
10200 | resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.9.tgz#b5d533d5092a6fc7d29eae1b43e4597d89bd527b" | ||
10201 | integrity sha512-HJeaJJQdSufi9Y5T7jlyyhkeq+mWPCog86q6ypoTi66boBMMJTo2abiOSHS9KaOGAJjH72gfvrjVY5FRdjlxYA== | ||
10202 | dependencies: | ||
10203 | global "^4.3.2" | ||
10204 | video.js "^6 || ^7" | ||
10205 | |||
10236 | videojs-dock@^2.0.2: | 10206 | videojs-dock@^2.0.2: |
10237 | version "2.1.4" | 10207 | version "2.1.4" |
10238 | resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.1.4.tgz#0ebd198b5d48990e3523fdc87dbfdb9fe96f804c" | 10208 | resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.1.4.tgz#0ebd198b5d48990e3523fdc87dbfdb9fe96f804c" |
@@ -10315,7 +10285,7 @@ watchpack@^1.5.0: | |||
10315 | graceful-fs "^4.1.2" | 10285 | graceful-fs "^4.1.2" |
10316 | neo-async "^2.5.0" | 10286 | neo-async "^2.5.0" |
10317 | 10287 | ||
10318 | wbuf@^1.1.0, wbuf@^1.7.2: | 10288 | wbuf@^1.1.0, wbuf@^1.7.3: |
10319 | version "1.7.3" | 10289 | version "1.7.3" |
10320 | resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" | 10290 | resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" |
10321 | integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== | 10291 | integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== |
@@ -10404,10 +10374,20 @@ webpack-dev-middleware@3.4.0: | |||
10404 | range-parser "^1.0.3" | 10374 | range-parser "^1.0.3" |
10405 | webpack-log "^2.0.0" | 10375 | webpack-log "^2.0.0" |
10406 | 10376 | ||
10407 | webpack-dev-server@3.1.10: | 10377 | webpack-dev-middleware@3.5.1: |
10408 | version "3.1.10" | 10378 | version "3.5.1" |
10409 | resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz#507411bee727ee8d2fdffdc621b66a64ab3dea2b" | 10379 | resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.5.1.tgz#9265b7742ef50f54f54c1d9af022fc17c1be9b88" |
10410 | integrity sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww== | 10380 | integrity sha512-4dwCh/AyMOYAybggUr8fiCkRnjVDp+Cqlr9c+aaNB3GJYgRGYQWJ1YX/WAKUNA9dPNHZ6QSN2lYDKqjKSI8Vqw== |
10381 | dependencies: | ||
10382 | memory-fs "~0.4.1" | ||
10383 | mime "^2.3.1" | ||
10384 | range-parser "^1.0.3" | ||
10385 | webpack-log "^2.0.0" | ||
10386 | |||
10387 | webpack-dev-server@3.1.14: | ||
10388 | version "3.1.14" | ||
10389 | resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz#60fb229b997fc5a0a1fc6237421030180959d469" | ||
10390 | integrity sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ== | ||
10411 | dependencies: | 10391 | dependencies: |
10412 | ansi-html "0.0.7" | 10392 | ansi-html "0.0.7" |
10413 | bonjour "^3.5.0" | 10393 | bonjour "^3.5.0" |
@@ -10428,12 +10408,14 @@ webpack-dev-server@3.1.10: | |||
10428 | portfinder "^1.0.9" | 10408 | portfinder "^1.0.9" |
10429 | schema-utils "^1.0.0" | 10409 | schema-utils "^1.0.0" |
10430 | selfsigned "^1.9.1" | 10410 | selfsigned "^1.9.1" |
10411 | semver "^5.6.0" | ||
10431 | serve-index "^1.7.2" | 10412 | serve-index "^1.7.2" |
10432 | sockjs "0.3.19" | 10413 | sockjs "0.3.19" |
10433 | sockjs-client "1.3.0" | 10414 | sockjs-client "1.3.0" |
10434 | spdy "^3.4.1" | 10415 | spdy "^4.0.0" |
10435 | strip-ansi "^3.0.0" | 10416 | strip-ansi "^3.0.0" |
10436 | supports-color "^5.1.0" | 10417 | supports-color "^5.1.0" |
10418 | url "^0.11.0" | ||
10437 | webpack-dev-middleware "3.4.0" | 10419 | webpack-dev-middleware "3.4.0" |
10438 | webpack-log "^2.0.0" | 10420 | webpack-log "^2.0.0" |
10439 | yargs "12.0.2" | 10421 | yargs "12.0.2" |
@@ -10456,21 +10438,13 @@ webpack-log@^2.0.0: | |||
10456 | ansi-colors "^3.0.0" | 10438 | ansi-colors "^3.0.0" |
10457 | uuid "^3.3.2" | 10439 | uuid "^3.3.2" |
10458 | 10440 | ||
10459 | webpack-merge@4.1.4: | 10441 | webpack-merge@4.2.1: |
10460 | version "4.1.4" | 10442 | version "4.2.1" |
10461 | resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.4.tgz#0fde38eabf2d5fd85251c24a5a8c48f8a3f4eb7b" | 10443 | resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4" |
10462 | integrity sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ== | 10444 | integrity sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw== |
10463 | dependencies: | 10445 | dependencies: |
10464 | lodash "^4.17.5" | 10446 | lodash "^4.17.5" |
10465 | 10447 | ||
10466 | webpack-sources@1.2.0: | ||
10467 | version "1.2.0" | ||
10468 | resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.2.0.tgz#18181e0d013fce096faf6f8e6d41eeffffdceac2" | ||
10469 | integrity sha512-9BZwxR85dNsjWz3blyxdOhTgtnQvv3OEs5xofI0wPYTwu5kaWxS08UuD1oI7WLBLpRO+ylf0ofnXLXWmGb2WMw== | ||
10470 | dependencies: | ||
10471 | source-list-map "^2.0.0" | ||
10472 | source-map "~0.6.1" | ||
10473 | |||
10474 | webpack-sources@1.3.0, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0: | 10448 | webpack-sources@1.3.0, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0: |
10475 | version "1.3.0" | 10449 | version "1.3.0" |
10476 | resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" | 10450 | resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" |
@@ -10494,47 +10468,17 @@ webpack-subresource-integrity@1.1.0-rc.6: | |||
10494 | dependencies: | 10468 | dependencies: |
10495 | webpack-core "^0.6.8" | 10469 | webpack-core "^0.6.8" |
10496 | 10470 | ||
10497 | webpack@4.23.1: | 10471 | webpack@4.29.0: |
10498 | version "4.23.1" | 10472 | version "4.29.0" |
10499 | resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.23.1.tgz#db7467b116771ae020c58bdfe2a0822785bb8239" | 10473 | resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.0.tgz#f2cfef83f7ae404ba889ff5d43efd285ca26e750" |
10500 | integrity sha512-iE5Cu4rGEDk7ONRjisTOjVHv3dDtcFfwitSxT7evtYj/rANJpt1OuC/Kozh1pBa99AUBr1L/LsaNB+D9Xz3CEg== | 10474 | integrity sha512-pxdGG0keDBtamE1mNvT5zyBdx+7wkh6mh7uzMOo/uRQ/fhsdj5FXkh/j5mapzs060forql1oXqXN9HJGju+y7w== |
10501 | dependencies: | ||
10502 | "@webassemblyjs/ast" "1.7.10" | ||
10503 | "@webassemblyjs/helper-module-context" "1.7.10" | ||
10504 | "@webassemblyjs/wasm-edit" "1.7.10" | ||
10505 | "@webassemblyjs/wasm-parser" "1.7.10" | ||
10506 | acorn "^5.6.2" | ||
10507 | acorn-dynamic-import "^3.0.0" | ||
10508 | ajv "^6.1.0" | ||
10509 | ajv-keywords "^3.1.0" | ||
10510 | chrome-trace-event "^1.0.0" | ||
10511 | enhanced-resolve "^4.1.0" | ||
10512 | eslint-scope "^4.0.0" | ||
10513 | json-parse-better-errors "^1.0.2" | ||
10514 | loader-runner "^2.3.0" | ||
10515 | loader-utils "^1.1.0" | ||
10516 | memory-fs "~0.4.1" | ||
10517 | micromatch "^3.1.8" | ||
10518 | mkdirp "~0.5.0" | ||
10519 | neo-async "^2.5.0" | ||
10520 | node-libs-browser "^2.0.0" | ||
10521 | schema-utils "^0.4.4" | ||
10522 | tapable "^1.1.0" | ||
10523 | uglifyjs-webpack-plugin "^1.2.4" | ||
10524 | watchpack "^1.5.0" | ||
10525 | webpack-sources "^1.3.0" | ||
10526 | |||
10527 | webpack@^4.17.1: | ||
10528 | version "4.26.1" | ||
10529 | resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.26.1.tgz#ff3a9283d363c07b3494dfa702d08f4f2ef6cb39" | ||
10530 | integrity sha512-i2oOvEvuvLLSuSCkdVrknaxAhtUZ9g+nLSoHCWV0gDzqGX2DXaCrMmMUpbRsTSSLrUqAI56PoEiyMUZIZ1msug== | ||
10531 | dependencies: | 10475 | dependencies: |
10532 | "@webassemblyjs/ast" "1.7.11" | 10476 | "@webassemblyjs/ast" "1.7.11" |
10533 | "@webassemblyjs/helper-module-context" "1.7.11" | 10477 | "@webassemblyjs/helper-module-context" "1.7.11" |
10534 | "@webassemblyjs/wasm-edit" "1.7.11" | 10478 | "@webassemblyjs/wasm-edit" "1.7.11" |
10535 | "@webassemblyjs/wasm-parser" "1.7.11" | 10479 | "@webassemblyjs/wasm-parser" "1.7.11" |
10536 | acorn "^5.6.2" | 10480 | acorn "^6.0.5" |
10537 | acorn-dynamic-import "^3.0.0" | 10481 | acorn-dynamic-import "^4.0.0" |
10538 | ajv "^6.1.0" | 10482 | ajv "^6.1.0" |
10539 | ajv-keywords "^3.1.0" | 10483 | ajv-keywords "^3.1.0" |
10540 | chrome-trace-event "^1.0.0" | 10484 | chrome-trace-event "^1.0.0" |
@@ -10662,7 +10606,7 @@ which-module@^2.0.0: | |||
10662 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" | 10606 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |
10663 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= | 10607 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= |
10664 | 10608 | ||
10665 | which@1, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.0, which@^1.3.1: | 10609 | which@1, which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.0, which@^1.3.1: |
10666 | version "1.3.1" | 10610 | version "1.3.1" |
10667 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" | 10611 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" |
10668 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== | 10612 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== |
@@ -10676,16 +10620,16 @@ wide-align@^1.1.0: | |||
10676 | dependencies: | 10620 | dependencies: |
10677 | string-width "^1.0.2 || 2" | 10621 | string-width "^1.0.2 || 2" |
10678 | 10622 | ||
10679 | wordwrap@^1.0.0, wordwrap@~1.0.0: | ||
10680 | version "1.0.0" | ||
10681 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" | ||
10682 | integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= | ||
10683 | |||
10684 | wordwrap@~0.0.2: | 10623 | wordwrap@~0.0.2: |
10685 | version "0.0.3" | 10624 | version "0.0.3" |
10686 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" | 10625 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" |
10687 | integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= | 10626 | integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= |
10688 | 10627 | ||
10628 | wordwrap@~1.0.0: | ||
10629 | version "1.0.0" | ||
10630 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" | ||
10631 | integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= | ||
10632 | |||
10689 | worker-farm@^1.5.2: | 10633 | worker-farm@^1.5.2: |
10690 | version "1.6.0" | 10634 | version "1.6.0" |
10691 | resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" | 10635 | resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" |
diff --git a/config/default.yaml b/config/default.yaml index e16b8c352..ad0e6084b 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -48,6 +48,7 @@ storage: | |||
48 | tmp: 'storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... | 48 | tmp: 'storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... |
49 | avatars: 'storage/avatars/' | 49 | avatars: 'storage/avatars/' |
50 | videos: 'storage/videos/' | 50 | videos: 'storage/videos/' |
51 | playlists: 'storage/playlists/' | ||
51 | redundancy: 'storage/redundancy/' | 52 | redundancy: 'storage/redundancy/' |
52 | logs: 'storage/logs/' | 53 | logs: 'storage/logs/' |
53 | previews: 'storage/previews/' | 54 | previews: 'storage/previews/' |
@@ -138,6 +139,14 @@ transcoding: | |||
138 | 480p: false | 139 | 480p: false |
139 | 720p: false | 140 | 720p: false |
140 | 1080p: false | 141 | 1080p: false |
142 | # /!\ EXPERIMENTAL /!\ | ||
143 | # Generate HLS playlist/segments. Better playback than with WebTorrent: | ||
144 | # * Resolution change is smoother | ||
145 | # * Faster playback in particular with long videos | ||
146 | # * More stable playback (less bugs/infinite loading) | ||
147 | # /!\ Multiply videos storage by two /!\ | ||
148 | hls: | ||
149 | enabled: false | ||
141 | 150 | ||
142 | import: | 151 | import: |
143 | # Add ability for your users to import remote videos (from YouTube, torrent...) | 152 | # Add ability for your users to import remote videos (from YouTube, torrent...) |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 661eac0d5..98734bab6 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -49,6 +49,7 @@ storage: | |||
49 | tmp: '/var/www/peertube/storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... | 49 | tmp: '/var/www/peertube/storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... |
50 | avatars: '/var/www/peertube/storage/avatars/' | 50 | avatars: '/var/www/peertube/storage/avatars/' |
51 | videos: '/var/www/peertube/storage/videos/' | 51 | videos: '/var/www/peertube/storage/videos/' |
52 | playlists: '/var/www/peertube/storage/playlists/' | ||
52 | redundancy: '/var/www/peertube/storage/videos/' | 53 | redundancy: '/var/www/peertube/storage/videos/' |
53 | logs: '/var/www/peertube/storage/logs/' | 54 | logs: '/var/www/peertube/storage/logs/' |
54 | previews: '/var/www/peertube/storage/previews/' | 55 | previews: '/var/www/peertube/storage/previews/' |
@@ -151,6 +152,14 @@ transcoding: | |||
151 | 480p: false | 152 | 480p: false |
152 | 720p: false | 153 | 720p: false |
153 | 1080p: false | 154 | 1080p: false |
155 | # /!\ EXPERIMENTAL /!\ | ||
156 | # Generate HLS playlist/segments. Better playback than with WebTorrent: | ||
157 | # * Resolution change is smoother | ||
158 | # * Faster playback in particular with long videos | ||
159 | # * More stable playback (less bugs/infinite loading) | ||
160 | # /!\ Multiply videos storage by two /!\ | ||
161 | hls: | ||
162 | enabled: false | ||
154 | 163 | ||
155 | import: | 164 | import: |
156 | # Add ability for your users to import remote videos (from YouTube, torrent...) | 165 | # Add ability for your users to import remote videos (from YouTube, torrent...) |
diff --git a/config/test-1.yaml b/config/test-1.yaml index 8f4f66d2a..fb69818f3 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test1/tmp/' | 13 | tmp: 'test1/tmp/' |
14 | avatars: 'test1/avatars/' | 14 | avatars: 'test1/avatars/' |
15 | videos: 'test1/videos/' | 15 | videos: 'test1/videos/' |
16 | playlists: 'test1/playlists/' | ||
16 | redundancy: 'test1/redundancy/' | 17 | redundancy: 'test1/redundancy/' |
17 | logs: 'test1/logs/' | 18 | logs: 'test1/logs/' |
18 | previews: 'test1/previews/' | 19 | previews: 'test1/previews/' |
diff --git a/config/test-2.yaml b/config/test-2.yaml index b6d319394..5caddaaa8 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test2/tmp/' | 13 | tmp: 'test2/tmp/' |
14 | avatars: 'test2/avatars/' | 14 | avatars: 'test2/avatars/' |
15 | videos: 'test2/videos/' | 15 | videos: 'test2/videos/' |
16 | playlists: 'test2/playlists/' | ||
16 | redundancy: 'test2/redundancy/' | 17 | redundancy: 'test2/redundancy/' |
17 | logs: 'test2/logs/' | 18 | logs: 'test2/logs/' |
18 | previews: 'test2/previews/' | 19 | previews: 'test2/previews/' |
diff --git a/config/test-3.yaml b/config/test-3.yaml index 934401eb0..fac7ebee1 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test3/tmp/' | 13 | tmp: 'test3/tmp/' |
14 | avatars: 'test3/avatars/' | 14 | avatars: 'test3/avatars/' |
15 | videos: 'test3/videos/' | 15 | videos: 'test3/videos/' |
16 | playlists: 'test3/playlists/' | ||
16 | redundancy: 'test3/redundancy/' | 17 | redundancy: 'test3/redundancy/' |
17 | logs: 'test3/logs/' | 18 | logs: 'test3/logs/' |
18 | previews: 'test3/previews/' | 19 | previews: 'test3/previews/' |
diff --git a/config/test-4.yaml b/config/test-4.yaml index ee99b250b..33033773a 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test4/tmp/' | 13 | tmp: 'test4/tmp/' |
14 | avatars: 'test4/avatars/' | 14 | avatars: 'test4/avatars/' |
15 | videos: 'test4/videos/' | 15 | videos: 'test4/videos/' |
16 | playlists: 'test4/playlists/' | ||
16 | redundancy: 'test4/redundancy/' | 17 | redundancy: 'test4/redundancy/' |
17 | logs: 'test4/logs/' | 18 | logs: 'test4/logs/' |
18 | previews: 'test4/previews/' | 19 | previews: 'test4/previews/' |
diff --git a/config/test-5.yaml b/config/test-5.yaml index e2662bdd9..d365b6f2b 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test5/tmp/' | 13 | tmp: 'test5/tmp/' |
14 | avatars: 'test5/avatars/' | 14 | avatars: 'test5/avatars/' |
15 | videos: 'test5/videos/' | 15 | videos: 'test5/videos/' |
16 | playlists: 'test5/playlists/' | ||
16 | redundancy: 'test5/redundancy/' | 17 | redundancy: 'test5/redundancy/' |
17 | logs: 'test5/logs/' | 18 | logs: 'test5/logs/' |
18 | previews: 'test5/previews/' | 19 | previews: 'test5/previews/' |
diff --git a/config/test-6.yaml b/config/test-6.yaml index ad39c6a9f..44541c003 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml | |||
@@ -13,6 +13,7 @@ storage: | |||
13 | tmp: 'test6/tmp/' | 13 | tmp: 'test6/tmp/' |
14 | avatars: 'test6/avatars/' | 14 | avatars: 'test6/avatars/' |
15 | videos: 'test6/videos/' | 15 | videos: 'test6/videos/' |
16 | playlists: 'test6/playlists/' | ||
16 | redundancy: 'test6/redundancy/' | 17 | redundancy: 'test6/redundancy/' |
17 | logs: 'test6/logs/' | 18 | logs: 'test6/logs/' |
18 | previews: 'test6/previews/' | 19 | previews: 'test6/previews/' |
diff --git a/config/test.yaml b/config/test.yaml index aba5dd73c..682530840 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -62,6 +62,8 @@ transcoding: | |||
62 | 480p: true | 62 | 480p: true |
63 | 720p: true | 63 | 720p: true |
64 | 1080p: true | 64 | 1080p: true |
65 | hls: | ||
66 | enabled: true | ||
65 | 67 | ||
66 | import: | 68 | import: |
67 | videos: | 69 | videos: |
diff --git a/package.json b/package.json index ea3f88e24..0cf39c7ee 100644 --- a/package.json +++ b/package.json | |||
@@ -1,7 +1,7 @@ | |||
1 | { | 1 | { |
2 | "name": "peertube", | 2 | "name": "peertube", |
3 | "description": "Federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.", | 3 | "description": "Federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.", |
4 | "version": "1.1.0", | 4 | "version": "1.2.0", |
5 | "private": true, | 5 | "private": true, |
6 | "licence": "AGPLv3", | 6 | "licence": "AGPLv3", |
7 | "engines": { | 7 | "engines": { |
diff --git a/scripts/dev/server.sh b/scripts/dev/server.sh index 9b8fddac6..b4675c57f 100755 --- a/scripts/dev/server.sh +++ b/scripts/dev/server.sh | |||
@@ -4,7 +4,7 @@ set -eu | |||
4 | 4 | ||
5 | if [ ! -f "./client/dist/en_US/index.html" ]; then | 5 | if [ ! -f "./client/dist/en_US/index.html" ]; then |
6 | echo "client/dist/en_US/index.html does not exist, compile client files..." | 6 | echo "client/dist/en_US/index.html does not exist, compile client files..." |
7 | npm run build:client | 7 | npm run build:client -- --light |
8 | fi | 8 | fi |
9 | 9 | ||
10 | npm run watch:server | 10 | npm run watch:server |
diff --git a/scripts/generate-code-contributors.ts b/scripts/generate-code-contributors.ts index 9824bc2f5..96110307a 100755 --- a/scripts/generate-code-contributors.ts +++ b/scripts/generate-code-contributors.ts | |||
@@ -41,7 +41,7 @@ async function run () { | |||
41 | } | 41 | } |
42 | 42 | ||
43 | function get (url: string, headers: any = {}) { | 43 | function get (url: string, headers: any = {}) { |
44 | return doRequest({ | 44 | return doRequest<any>({ |
45 | uri: url, | 45 | uri: url, |
46 | json: true, | 46 | json: true, |
47 | headers: Object.assign(headers, { | 47 | headers: Object.assign(headers, { |
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index ab28f94c8..664207e1c 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts | |||
@@ -23,12 +23,15 @@ const playerKeys = { | |||
23 | 'Speed': 'Speed', | 23 | 'Speed': 'Speed', |
24 | 'Subtitles/CC': 'Subtitles/CC', | 24 | 'Subtitles/CC': 'Subtitles/CC', |
25 | 'peers': 'peers', | 25 | 'peers': 'peers', |
26 | 'peer': 'peer', | ||
26 | 'Go to the video page': 'Go to the video page', | 27 | 'Go to the video page': 'Go to the video page', |
27 | 'Settings': 'Settings', | 28 | 'Settings': 'Settings', |
28 | 'Uses P2P, others may know you are watching this video.': 'Uses P2P, others may know you are watching this video.', | 29 | 'Uses P2P, others may know you are watching this video.': 'Uses P2P, others may know you are watching this video.', |
29 | 'Copy the video URL': 'Copy the video URL', | 30 | 'Copy the video URL': 'Copy the video URL', |
30 | 'Copy the video URL at the current time': 'Copy the video URL at the current time', | 31 | 'Copy the video URL at the current time': 'Copy the video URL at the current time', |
31 | 'Copy embed code': 'Copy embed code' | 32 | 'Copy embed code': 'Copy embed code', |
33 | 'Total downloaded: ': 'Total downloaded: ', | ||
34 | 'Total uploaded: ': 'Total uploaded: ' | ||
32 | } | 35 | } |
33 | const playerTranslations = { | 36 | const playerTranslations = { |
34 | target: join(__dirname, '../../../client/src/locale/source/player_en_US.xml'), | 37 | target: join(__dirname, '../../../client/src/locale/source/player_en_US.xml'), |
diff --git a/scripts/update-host.ts b/scripts/update-host.ts index 422a3c9a7..64eba867a 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts | |||
@@ -13,6 +13,7 @@ import { VideoCommentModel } from '../server/models/video/video-comment' | |||
13 | import { getServerActor } from '../server/helpers/utils' | 13 | import { getServerActor } from '../server/helpers/utils' |
14 | import { AccountModel } from '../server/models/account/account' | 14 | import { AccountModel } from '../server/models/account/account' |
15 | import { VideoChannelModel } from '../server/models/video/video-channel' | 15 | import { VideoChannelModel } from '../server/models/video/video-channel' |
16 | import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist' | ||
16 | 17 | ||
17 | run() | 18 | run() |
18 | .then(() => process.exit(0)) | 19 | .then(() => process.exit(0)) |
@@ -109,11 +110,9 @@ async function run () { | |||
109 | 110 | ||
110 | console.log('Updating video and torrent files.') | 111 | console.log('Updating video and torrent files.') |
111 | 112 | ||
112 | const videos = await VideoModel.list() | 113 | const videos = await VideoModel.listLocal() |
113 | for (const video of videos) { | 114 | for (const video of videos) { |
114 | if (video.isOwned() === false) continue | 115 | console.log('Updating video ' + video.uuid) |
115 | |||
116 | console.log('Updated video ' + video.uuid) | ||
117 | 116 | ||
118 | video.url = getVideoActivityPubUrl(video) | 117 | video.url = getVideoActivityPubUrl(video) |
119 | await video.save() | 118 | await video.save() |
@@ -122,5 +121,12 @@ async function run () { | |||
122 | console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid) | 121 | console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid) |
123 | await video.createTorrentAndSetInfoHash(file) | 122 | await video.createTorrentAndSetInfoHash(file) |
124 | } | 123 | } |
124 | |||
125 | for (const playlist of video.VideoStreamingPlaylists) { | ||
126 | playlist.playlistUrl = CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
127 | playlist.segmentsSha256Url = CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid) | ||
128 | |||
129 | await playlist.save() | ||
130 | } | ||
125 | } | 131 | } |
126 | } | 132 | } |
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 7e87f6f3b..31c0a5fbd 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -33,7 +33,7 @@ import { | |||
33 | getVideoSharesActivityPubUrl | 33 | getVideoSharesActivityPubUrl |
34 | } from '../../lib/activitypub' | 34 | } from '../../lib/activitypub' |
35 | import { VideoCaptionModel } from '../../models/video/video-caption' | 35 | import { VideoCaptionModel } from '../../models/video/video-caption' |
36 | import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' | 36 | import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' |
37 | import { getServerActor } from '../../helpers/utils' | 37 | import { getServerActor } from '../../helpers/utils' |
38 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 38 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
39 | import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' | 39 | import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' |
@@ -63,11 +63,11 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', | |||
63 | 63 | ||
64 | activityPubClientRouter.get('/videos/watch/:id', | 64 | activityPubClientRouter.get('/videos/watch/:id', |
65 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), | 65 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), |
66 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 66 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
67 | executeIfActivityPub(asyncMiddleware(videoController)) | 67 | executeIfActivityPub(asyncMiddleware(videoController)) |
68 | ) | 68 | ) |
69 | activityPubClientRouter.get('/videos/watch/:id/activity', | 69 | activityPubClientRouter.get('/videos/watch/:id/activity', |
70 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 70 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
71 | executeIfActivityPub(asyncMiddleware(videoController)) | 71 | executeIfActivityPub(asyncMiddleware(videoController)) |
72 | ) | 72 | ) |
73 | activityPubClientRouter.get('/videos/watch/:id/announces', | 73 | activityPubClientRouter.get('/videos/watch/:id/announces', |
@@ -113,7 +113,11 @@ activityPubClientRouter.get('/video-channels/:name/following', | |||
113 | ) | 113 | ) |
114 | 114 | ||
115 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', | 115 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', |
116 | executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), | 116 | executeIfActivityPub(asyncMiddleware(videoFileRedundancyGetValidator)), |
117 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | ||
118 | ) | ||
119 | activityPubClientRouter.get('/redundancy/video-playlists/:streamingPlaylistType/:videoId', | ||
120 | executeIfActivityPub(asyncMiddleware(videoPlaylistRedundancyGetValidator)), | ||
117 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | 121 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) |
118 | ) | 122 | ) |
119 | 123 | ||
@@ -160,7 +164,8 @@ function getAccountVideoRate (rateType: VideoRateType) { | |||
160 | } | 164 | } |
161 | 165 | ||
162 | async function videoController (req: express.Request, res: express.Response) { | 166 | async function videoController (req: express.Request, res: express.Response) { |
163 | const video: VideoModel = res.locals.video | 167 | // We need more attributes |
168 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id) | ||
164 | 169 | ||
165 | if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) | 170 | if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) |
166 | 171 | ||
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 255026f46..1f3341bc0 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { omit, snakeCase } from 'lodash' | 2 | import { snakeCase } from 'lodash' |
3 | import { ServerConfig, UserRight } from '../../../shared' | 3 | import { ServerConfig, UserRight } from '../../../shared' |
4 | import { About } from '../../../shared/models/server/about.model' | 4 | import { About } from '../../../shared/models/server/about.model' |
5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
@@ -78,6 +78,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
78 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 78 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION |
79 | }, | 79 | }, |
80 | transcoding: { | 80 | transcoding: { |
81 | hls: { | ||
82 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
83 | }, | ||
81 | enabledResolutions | 84 | enabledResolutions |
82 | }, | 85 | }, |
83 | import: { | 86 | import: { |
@@ -246,6 +249,9 @@ function customConfig (): CustomConfig { | |||
246 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], | 249 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], |
247 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], | 250 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], |
248 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] | 251 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] |
252 | }, | ||
253 | hls: { | ||
254 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
249 | } | 255 | } |
250 | }, | 256 | }, |
251 | import: { | 257 | import: { |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index dbe0718d4..e3533a7f6 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -229,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e | |||
229 | return res.status(204).end() | 229 | return res.status(204).end() |
230 | } | 230 | } |
231 | 231 | ||
232 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 232 | async function blockUser (req: express.Request, res: express.Response) { |
233 | const user: UserModel = res.locals.user | 233 | const user: UserModel = res.locals.user |
234 | const reason = req.body.reason | 234 | const reason = req.body.reason |
235 | 235 | ||
@@ -238,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp | |||
238 | return res.status(204).end() | 238 | return res.status(204).end() |
239 | } | 239 | } |
240 | 240 | ||
241 | function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 241 | function getUser (req: express.Request, res: express.Response) { |
242 | return res.json((res.locals.user as UserModel).toFormattedJSON()) | 242 | return res.json((res.locals.user as UserModel).toFormattedJSON()) |
243 | } | 243 | } |
244 | 244 | ||
245 | async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 245 | async function autocompleteUsers (req: express.Request, res: express.Response) { |
246 | const resultList = await UserModel.autoComplete(req.query.search as string) | 246 | const resultList = await UserModel.autoComplete(req.query.search as string) |
247 | 247 | ||
248 | return res.json(resultList) | 248 | return res.json(resultList) |
249 | } | 249 | } |
250 | 250 | ||
251 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 251 | async function listUsers (req: express.Request, res: express.Response) { |
252 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) | 252 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) |
253 | 253 | ||
254 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 254 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
255 | } | 255 | } |
256 | 256 | ||
257 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 257 | async function removeUser (req: express.Request, res: express.Response) { |
258 | const user: UserModel = res.locals.user | 258 | const user: UserModel = res.locals.user |
259 | 259 | ||
260 | await user.destroy() | 260 | await user.destroy() |
@@ -264,12 +264,13 @@ async function removeUser (req: express.Request, res: express.Response, next: ex | |||
264 | return res.sendStatus(204) | 264 | return res.sendStatus(204) |
265 | } | 265 | } |
266 | 266 | ||
267 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 267 | async function updateUser (req: express.Request, res: express.Response) { |
268 | const body: UserUpdate = req.body | 268 | const body: UserUpdate = req.body |
269 | const userToUpdate = res.locals.user as UserModel | 269 | const userToUpdate = res.locals.user as UserModel |
270 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | 270 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) |
271 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 271 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
272 | 272 | ||
273 | if (body.password !== undefined) userToUpdate.password = body.password | ||
273 | if (body.email !== undefined) userToUpdate.email = body.email | 274 | if (body.email !== undefined) userToUpdate.email = body.email |
274 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | 275 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified |
275 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 276 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota |
@@ -279,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex | |||
279 | const user = await userToUpdate.save() | 280 | const user = await userToUpdate.save() |
280 | 281 | ||
281 | // Destroy user token to refresh rights | 282 | // Destroy user token to refresh rights |
282 | if (roleChanged) await deleteUserToken(userToUpdate.id) | 283 | if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) |
283 | 284 | ||
284 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) | 285 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) |
285 | 286 | ||
286 | // Don't need to send this update to followers, these attributes are not propagated | 287 | // Don't need to send this update to followers, these attributes are not federated |
287 | 288 | ||
288 | return res.sendStatus(204) | 289 | return res.sendStatus(204) |
289 | } | 290 | } |
@@ -293,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response | |||
293 | 294 | ||
294 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) | 295 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) |
295 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString | 296 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString |
296 | await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) | 297 | await Emailer.Instance.addPasswordResetEmailJob(user.email, url) |
297 | 298 | ||
298 | return res.status(204).end() | 299 | return res.status(204).end() |
299 | } | 300 | } |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 94a2b8732..d5e154869 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -167,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) { | |||
167 | return res.sendStatus(204) | 167 | return res.sendStatus(204) |
168 | } | 168 | } |
169 | 169 | ||
170 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | 170 | async function updateMe (req: express.Request, res: express.Response) { |
171 | const body: UserUpdateMe = req.body | 171 | const body: UserUpdateMe = req.body |
172 | 172 | ||
173 | const user: UserModel = res.locals.oauth.token.user | 173 | const user: UserModel = res.locals.oauth.token.user |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 8414ca42c..3b1c2d255 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -36,6 +36,7 @@ import { | |||
36 | setDefaultPagination, | 36 | setDefaultPagination, |
37 | setDefaultSort, | 37 | setDefaultSort, |
38 | videosAddValidator, | 38 | videosAddValidator, |
39 | videosCustomGetValidator, | ||
39 | videosGetValidator, | 40 | videosGetValidator, |
40 | videosRemoveValidator, | 41 | videosRemoveValidator, |
41 | videosSortValidator, | 42 | videosSortValidator, |
@@ -123,9 +124,9 @@ videosRouter.get('/:id/description', | |||
123 | ) | 124 | ) |
124 | videosRouter.get('/:id', | 125 | videosRouter.get('/:id', |
125 | optionalAuthenticate, | 126 | optionalAuthenticate, |
126 | asyncMiddleware(videosGetValidator), | 127 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), |
127 | asyncMiddleware(checkVideoFollowConstraints), | 128 | asyncMiddleware(checkVideoFollowConstraints), |
128 | getVideo | 129 | asyncMiddleware(getVideo) |
129 | ) | 130 | ) |
130 | videosRouter.post('/:id/views', | 131 | videosRouter.post('/:id/views', |
131 | asyncMiddleware(videosGetValidator), | 132 | asyncMiddleware(videosGetValidator), |
@@ -395,15 +396,17 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
395 | return res.type('json').status(204).end() | 396 | return res.type('json').status(204).end() |
396 | } | 397 | } |
397 | 398 | ||
398 | function getVideo (req: express.Request, res: express.Response) { | 399 | async function getVideo (req: express.Request, res: express.Response) { |
399 | const videoInstance = res.locals.video | 400 | // We need more attributes |
401 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null | ||
402 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId) | ||
400 | 403 | ||
401 | if (videoInstance.isOutdated()) { | 404 | if (video.isOutdated()) { |
402 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoInstance.url } }) | 405 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) |
403 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err })) | 406 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err })) |
404 | } | 407 | } |
405 | 408 | ||
406 | return res.json(videoInstance.toFormattedDetailsJSON()) | 409 | return res.json(video.toFormattedDetailsJSON()) |
407 | } | 410 | } |
408 | 411 | ||
409 | async function viewVideo (req: express.Request, res: express.Response) { | 412 | async function viewVideo (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 4fd58f70c..b21f9da00 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' | 3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' |
4 | import { VideosPreviewCache } from '../lib/cache' | 4 | import { VideosPreviewCache } from '../lib/cache' |
5 | import { cacheRoute } from '../middlewares/cache' | 5 | import { cacheRoute } from '../middlewares/cache' |
6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' | 6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' |
@@ -51,6 +51,13 @@ staticRouter.use( | |||
51 | asyncMiddleware(downloadVideoFile) | 51 | asyncMiddleware(downloadVideoFile) |
52 | ) | 52 | ) |
53 | 53 | ||
54 | // HLS | ||
55 | staticRouter.use( | ||
56 | STATIC_PATHS.PLAYLISTS.HLS, | ||
57 | cors(), | ||
58 | express.static(HLS_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist | ||
59 | ) | ||
60 | |||
54 | // Thumbnails path for express | 61 | // Thumbnails path for express |
55 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR | 62 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR |
56 | staticRouter.use( | 63 | staticRouter.use( |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 1deb8c402..8b77d9de7 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -7,6 +7,7 @@ import { Server as WebSocketServer } from 'ws' | |||
7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' | 7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' |
8 | import { VideoFileModel } from '../models/video/video-file' | 8 | import { VideoFileModel } from '../models/video/video-file' |
9 | import { parse } from 'url' | 9 | import { parse } from 'url' |
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
10 | 11 | ||
11 | const TrackerServer = bitTorrentTracker.Server | 12 | const TrackerServer = bitTorrentTracker.Server |
12 | 13 | ||
@@ -21,7 +22,7 @@ const trackerServer = new TrackerServer({ | |||
21 | udp: false, | 22 | udp: false, |
22 | ws: false, | 23 | ws: false, |
23 | dht: false, | 24 | dht: false, |
24 | filter: function (infoHash, params, cb) { | 25 | filter: async function (infoHash, params, cb) { |
25 | let ip: string | 26 | let ip: string |
26 | 27 | ||
27 | if (params.type === 'ws') { | 28 | if (params.type === 'ws') { |
@@ -32,19 +33,25 @@ const trackerServer = new TrackerServer({ | |||
32 | 33 | ||
33 | const key = ip + '-' + infoHash | 34 | const key = ip + '-' + infoHash |
34 | 35 | ||
35 | peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 | 36 | peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1 |
36 | peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 | 37 | peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1 |
37 | 38 | ||
38 | if (peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { | 39 | if (peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { |
39 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) | 40 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) |
40 | } | 41 | } |
41 | 42 | ||
42 | VideoFileModel.isInfohashExists(infoHash) | 43 | try { |
43 | .then(exists => { | 44 | const videoFileExists = await VideoFileModel.doesInfohashExist(infoHash) |
44 | if (exists === false) return cb(new Error(`Unknown infoHash ${infoHash}`)) | 45 | if (videoFileExists === true) return cb() |
45 | 46 | ||
46 | return cb() | 47 | const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash) |
47 | }) | 48 | if (playlistExists === true) return cb() |
49 | |||
50 | return cb(new Error(`Unknown infoHash ${infoHash}`)) | ||
51 | } catch (err) { | ||
52 | logger.error('Error in tracker filter.', { err }) | ||
53 | return cb(err) | ||
54 | } | ||
48 | } | 55 | } |
49 | }) | 56 | }) |
50 | 57 | ||
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index f1430055f..eba552524 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -15,7 +15,7 @@ function activityPubContextify <T> (data: T) { | |||
15 | 'https://w3id.org/security/v1', | 15 | 'https://w3id.org/security/v1', |
16 | { | 16 | { |
17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', | 17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', |
18 | pt: 'https://joinpeertube.org/ns', | 18 | pt: 'https://joinpeertube.org/ns#', |
19 | sc: 'http://schema.org#', | 19 | sc: 'http://schema.org#', |
20 | Hashtag: 'as:Hashtag', | 20 | Hashtag: 'as:Hashtag', |
21 | uuid: 'sc:identifier', | 21 | uuid: 'sc:identifier', |
@@ -32,7 +32,8 @@ function activityPubContextify <T> (data: T) { | |||
32 | waitTranscoding: 'sc:Boolean', | 32 | waitTranscoding: 'sc:Boolean', |
33 | expires: 'sc:expires', | 33 | expires: 'sc:expires', |
34 | support: 'sc:Text', | 34 | support: 'sc:Text', |
35 | CacheFile: 'pt:CacheFile' | 35 | CacheFile: 'pt:CacheFile', |
36 | Infohash: 'pt:Infohash' | ||
36 | }, | 37 | }, |
37 | { | 38 | { |
38 | likes: { | 39 | likes: { |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 3fb824e36..f38b82d97 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -193,10 +193,14 @@ function peertubeTruncate (str: string, maxLength: number) { | |||
193 | return truncate(str, options) | 193 | return truncate(str, options) |
194 | } | 194 | } |
195 | 195 | ||
196 | function sha256 (str: string, encoding: HexBase64Latin1Encoding = 'hex') { | 196 | function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { |
197 | return createHash('sha256').update(str).digest(encoding) | 197 | return createHash('sha256').update(str).digest(encoding) |
198 | } | 198 | } |
199 | 199 | ||
200 | function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { | ||
201 | return createHash('sha1').update(str).digest(encoding) | ||
202 | } | ||
203 | |||
200 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 204 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
201 | return function promisified (): Promise<A> { | 205 | return function promisified (): Promise<A> { |
202 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | 206 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { |
@@ -262,7 +266,9 @@ export { | |||
262 | sanitizeHost, | 266 | sanitizeHost, |
263 | buildPath, | 267 | buildPath, |
264 | peertubeTruncate, | 268 | peertubeTruncate, |
269 | |||
265 | sha256, | 270 | sha256, |
271 | sha1, | ||
266 | 272 | ||
267 | promisify0, | 273 | promisify0, |
268 | promisify1, | 274 | promisify1, |
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index e2bd0c55e..21d5c53ca 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -8,9 +8,19 @@ function isCacheFileObjectValid (object: CacheFileObject) { | |||
8 | object.type === 'CacheFile' && | 8 | object.type === 'CacheFile' && |
9 | isDateValid(object.expires) && | 9 | isDateValid(object.expires) && |
10 | isActivityPubUrlValid(object.object) && | 10 | isActivityPubUrlValid(object.object) && |
11 | isRemoteVideoUrlValid(object.url) | 11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) |
12 | } | 12 | } |
13 | 13 | ||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
14 | export { | 16 | export { |
15 | isCacheFileObjectValid | 17 | isCacheFileObjectValid |
16 | } | 18 | } |
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | function isPlaylistRedundancyUrlValid (url: any) { | ||
23 | return url.type === 'Link' && | ||
24 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
25 | isActivityPubUrlValid(url.href) | ||
26 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 0f34aab21..ad99c2724 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | 2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' |
3 | import { peertubeTruncate } from '../../core-utils' | 3 | import { peertubeTruncate } from '../../core-utils' |
4 | import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 4 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
5 | import { | 5 | import { |
6 | isVideoDurationValid, | 6 | isVideoDurationValid, |
7 | isVideoNameValid, | 7 | isVideoNameValid, |
@@ -12,7 +12,6 @@ import { | |||
12 | } from '../videos' | 12 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
16 | 15 | ||
17 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 16 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
18 | return isBaseActivityValid(activity, 'Update') && | 17 | return isBaseActivityValid(activity, 'Update') && |
@@ -81,6 +80,11 @@ function isRemoteVideoUrlValid (url: any) { | |||
81 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && | 80 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
82 | validator.isLength(url.href, { min: 5 }) && | 81 | validator.isLength(url.href, { min: 5 }) && |
83 | validator.isInt(url.height + '', { min: 0 }) | 82 | validator.isInt(url.height + '', { min: 0 }) |
83 | ) || | ||
84 | ( | ||
85 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
86 | isActivityPubUrlValid(url.href) && | ||
87 | isArray(url.tag) | ||
84 | ) | 88 | ) |
85 | } | 89 | } |
86 | 90 | ||
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index b6f0ebe6f..76647fea2 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -13,6 +13,10 @@ function isNotEmptyIntArray (value: any) { | |||
13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 | 13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 |
14 | } | 14 | } |
15 | 15 | ||
16 | function isArrayOf (value: any, validator: (value: any) => boolean) { | ||
17 | return isArray(value) && value.every(v => validator(v)) | ||
18 | } | ||
19 | |||
16 | function isDateValid (value: string) { | 20 | function isDateValid (value: string) { |
17 | return exists(value) && validator.isISO8601(value) | 21 | return exists(value) && validator.isISO8601(value) |
18 | } | 22 | } |
@@ -82,6 +86,7 @@ function isFileValid ( | |||
82 | 86 | ||
83 | export { | 87 | export { |
84 | exists, | 88 | exists, |
89 | isArrayOf, | ||
85 | isNotEmptyIntArray, | 90 | isNotEmptyIntArray, |
86 | isArray, | 91 | isArray, |
87 | isIdValid, | 92 | isIdValid, |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index e6f22e6c5..95e256b8f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -88,8 +88,8 @@ function isVideoFileExtnameValid (value: string) { | |||
88 | 88 | ||
89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
91 | .map(m => `(${m})`) | 91 | .map(m => `(${m})`) |
92 | .join('|') | 92 | .join('|') |
93 | 93 | ||
94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) | 94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) |
95 | } | 95 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index c7296054d..133b1b03b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
29 | return resolutionsEnabled | 29 | return resolutionsEnabled |
30 | } | 30 | } |
31 | 31 | ||
32 | async function getVideoFileResolution (path: string) { | 32 | async function getVideoFileSize (path: string) { |
33 | const videoStream = await getVideoFileStream(path) | 33 | const videoStream = await getVideoFileStream(path) |
34 | 34 | ||
35 | return { | 35 | return { |
36 | videoFileResolution: Math.min(videoStream.height, videoStream.width), | 36 | width: videoStream.width, |
37 | isPortraitMode: videoStream.height > videoStream.width | 37 | height: videoStream.height |
38 | } | ||
39 | } | ||
40 | |||
41 | async function getVideoFileResolution (path: string) { | ||
42 | const size = await getVideoFileSize(path) | ||
43 | |||
44 | return { | ||
45 | videoFileResolution: Math.min(size.height, size.width), | ||
46 | isPortraitMode: size.height > size.width | ||
38 | } | 47 | } |
39 | } | 48 | } |
40 | 49 | ||
41 | async function getVideoFileFPS (path: string) { | 50 | async function getVideoFileFPS (path: string) { |
42 | const videoStream = await getVideoFileStream(path) | 51 | const videoStream = await getVideoFileStream(path) |
43 | 52 | ||
44 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { | 53 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
45 | const valuesText: string = videoStream[key] | 54 | const valuesText: string = videoStream[key] |
46 | if (!valuesText) continue | 55 | if (!valuesText) continue |
47 | 56 | ||
@@ -110,8 +119,12 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
110 | type TranscodeOptions = { | 119 | type TranscodeOptions = { |
111 | inputPath: string | 120 | inputPath: string |
112 | outputPath: string | 121 | outputPath: string |
113 | resolution?: VideoResolution | 122 | resolution: VideoResolution |
114 | isPortraitMode?: boolean | 123 | isPortraitMode?: boolean |
124 | |||
125 | hlsPlaylist?: { | ||
126 | videoFilename: string | ||
127 | } | ||
115 | } | 128 | } |
116 | 129 | ||
117 | function transcode (options: TranscodeOptions) { | 130 | function transcode (options: TranscodeOptions) { |
@@ -150,6 +163,18 @@ function transcode (options: TranscodeOptions) { | |||
150 | command = command.withFPS(fps) | 163 | command = command.withFPS(fps) |
151 | } | 164 | } |
152 | 165 | ||
166 | if (options.hlsPlaylist) { | ||
167 | const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | ||
168 | |||
169 | command = command.outputOption('-hls_time 4') | ||
170 | .outputOption('-hls_list_size 0') | ||
171 | .outputOption('-hls_playlist_type vod') | ||
172 | .outputOption('-hls_segment_filename ' + videoPath) | ||
173 | .outputOption('-hls_segment_type fmp4') | ||
174 | .outputOption('-f hls') | ||
175 | .outputOption('-hls_flags single_file') | ||
176 | } | ||
177 | |||
153 | command | 178 | command |
154 | .on('error', (err, stdout, stderr) => { | 179 | .on('error', (err, stdout, stderr) => { |
155 | logger.error('Error in transcoding job.', { stdout, stderr }) | 180 | logger.error('Error in transcoding job.', { stdout, stderr }) |
@@ -166,6 +191,7 @@ function transcode (options: TranscodeOptions) { | |||
166 | // --------------------------------------------------------------------------- | 191 | // --------------------------------------------------------------------------- |
167 | 192 | ||
168 | export { | 193 | export { |
194 | getVideoFileSize, | ||
169 | getVideoFileResolution, | 195 | getVideoFileResolution, |
170 | getDurationFromVideoFile, | 196 | getDurationFromVideoFile, |
171 | generateImageFromVideoFile, | 197 | generateImageFromVideoFile, |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 3fc776f1a..5c6dc5e19 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -7,7 +7,7 @@ import { join } from 'path' | |||
7 | 7 | ||
8 | function doRequest <T> ( | 8 | function doRequest <T> ( |
9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
10 | ): Bluebird<{ response: request.RequestResponse, body: any }> { | 10 | ): Bluebird<{ response: request.RequestResponse, body: T }> { |
11 | if (requestOptions.activityPub === true) { | 11 | if (requestOptions.activityPub === true) { |
12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} | 12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} |
13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 3c3406e38..cb0e823c5 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -7,7 +7,6 @@ import { join } from 'path' | |||
7 | import { Instance as ParseTorrent } from 'parse-torrent' | 7 | import { Instance as ParseTorrent } from 'parse-torrent' |
8 | import { remove } from 'fs-extra' | 8 | import { remove } from 'fs-extra' |
9 | import * as memoizee from 'memoizee' | 9 | import * as memoizee from 'memoizee' |
10 | import { isArray } from './custom-validators/misc' | ||
11 | 10 | ||
12 | function deleteFileAsync (path: string) { | 11 | function deleteFileAsync (path: string) { |
13 | remove(path) | 12 | remove(path) |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 1bd21467d..c90fe06c7 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) | ||
9 | |||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 10 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 11 | ||
10 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | 12 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 7905d9ffa..29fdb263e 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -12,7 +12,7 @@ function checkMissedConfig () { | |||
12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', | 12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', |
13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
15 | 'storage.redundancy', 'storage.tmp', | 15 | 'storage.redundancy', 'storage.tmp', 'storage.playlists', |
16 | 'log.level', | 16 | 'log.level', |
17 | 'user.video_quota', 'user.video_quota_daily', | 17 | 'user.video_quota', 'user.video_quota_daily', |
18 | 'cache.previews.size', 'admin.email', 'contact_form.enabled', | 18 | 'cache.previews.size', 'admin.email', 'contact_form.enabled', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 93fdd3f03..e5c4c4e63 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -16,7 +16,7 @@ let config: IConfig = require('config') | |||
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | const LAST_MIGRATION_VERSION = 325 | 19 | const LAST_MIGRATION_VERSION = 330 |
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
@@ -192,6 +192,7 @@ const CONFIG = { | |||
192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), |
193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
195 | PLAYLISTS_DIR: buildPath(config.get<string>('storage.playlists')), | ||
195 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | 196 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), |
196 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 197 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
197 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 198 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
@@ -259,6 +260,9 @@ const CONFIG = { | |||
259 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, | 260 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, |
260 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, | 261 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, |
261 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } | 262 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } |
263 | }, | ||
264 | HLS: { | ||
265 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | ||
262 | } | 266 | } |
263 | }, | 267 | }, |
264 | IMPORT: { | 268 | IMPORT: { |
@@ -590,6 +594,9 @@ const STATIC_PATHS = { | |||
590 | TORRENTS: '/static/torrents/', | 594 | TORRENTS: '/static/torrents/', |
591 | WEBSEED: '/static/webseed/', | 595 | WEBSEED: '/static/webseed/', |
592 | REDUNDANCY: '/static/redundancy/', | 596 | REDUNDANCY: '/static/redundancy/', |
597 | PLAYLISTS: { | ||
598 | HLS: '/static/playlists/hls' | ||
599 | }, | ||
593 | AVATARS: '/static/avatars/', | 600 | AVATARS: '/static/avatars/', |
594 | VIDEO_CAPTIONS: '/static/video-captions/' | 601 | VIDEO_CAPTIONS: '/static/video-captions/' |
595 | } | 602 | } |
@@ -632,6 +639,9 @@ const CACHE = { | |||
632 | } | 639 | } |
633 | } | 640 | } |
634 | 641 | ||
642 | const HLS_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.PLAYLISTS_DIR, 'hls') | ||
643 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | ||
644 | |||
635 | const MEMOIZE_TTL = { | 645 | const MEMOIZE_TTL = { |
636 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours | 646 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours |
637 | } | 647 | } |
@@ -701,6 +711,8 @@ if (isTestInstance() === true) { | |||
701 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 711 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
702 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 | 712 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 |
703 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' | 713 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' |
714 | |||
715 | RATES_LIMIT.LOGIN.MAX = 20 | ||
704 | } | 716 | } |
705 | 717 | ||
706 | updateWebserverUrls() | 718 | updateWebserverUrls() |
@@ -709,6 +721,7 @@ updateWebserverUrls() | |||
709 | 721 | ||
710 | export { | 722 | export { |
711 | API_VERSION, | 723 | API_VERSION, |
724 | HLS_REDUNDANCY_DIRECTORY, | ||
712 | AVATARS_SIZE, | 725 | AVATARS_SIZE, |
713 | ACCEPT_HEADERS, | 726 | ACCEPT_HEADERS, |
714 | BCRYPT_SALT_SIZE, | 727 | BCRYPT_SALT_SIZE, |
@@ -733,6 +746,7 @@ export { | |||
733 | PRIVATE_RSA_KEY_SIZE, | 746 | PRIVATE_RSA_KEY_SIZE, |
734 | ROUTE_CACHE_LIFETIME, | 747 | ROUTE_CACHE_LIFETIME, |
735 | SORTABLE_COLUMNS, | 748 | SORTABLE_COLUMNS, |
749 | HLS_PLAYLIST_DIRECTORY, | ||
736 | FEEDS, | 750 | FEEDS, |
737 | JOB_TTL, | 751 | JOB_TTL, |
738 | NSFW_POLICY_TYPES, | 752 | NSFW_POLICY_TYPES, |
@@ -795,7 +809,9 @@ function buildVideoMimetypeExt () { | |||
795 | 'video/quicktime': '.mov', | 809 | 'video/quicktime': '.mov', |
796 | 'video/x-msvideo': '.avi', | 810 | 'video/x-msvideo': '.avi', |
797 | 'video/x-flv': '.flv', | 811 | 'video/x-flv': '.flv', |
798 | 'video/x-matroska': '.mkv' | 812 | 'video/x-matroska': '.mkv', |
813 | 'application/octet-stream': '.mkv', | ||
814 | 'video/avi': '.avi' | ||
799 | }) | 815 | }) |
800 | } | 816 | } |
801 | 817 | ||
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 84ad2079b..fe296142d 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -33,6 +33,7 @@ import { AccountBlocklistModel } from '../models/account/account-blocklist' | |||
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
34 | import { UserNotificationModel } from '../models/account/user-notification' | 34 | import { UserNotificationModel } from '../models/account/user-notification' |
35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' |
36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
36 | 37 | ||
37 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 38 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
38 | 39 | ||
@@ -99,7 +100,8 @@ async function initDatabaseModels (silent: boolean) { | |||
99 | AccountBlocklistModel, | 100 | AccountBlocklistModel, |
100 | ServerBlocklistModel, | 101 | ServerBlocklistModel, |
101 | UserNotificationModel, | 102 | UserNotificationModel, |
102 | UserNotificationSettingModel | 103 | UserNotificationSettingModel, |
104 | VideoStreamingPlaylistModel | ||
103 | ]) | 105 | ]) |
104 | 106 | ||
105 | // Check extensions exist in the database | 107 | // Check extensions exist in the database |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index b9a9da183..2b22e16fe 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -6,7 +6,7 @@ import { UserModel } from '../models/account/user' | |||
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
9 | import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' | 9 | import { CACHE, CONFIG, HLS_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' |
10 | import { sequelizeTypescript } from './database' | 10 | import { sequelizeTypescript } from './database' |
11 | import { remove, ensureDir } from 'fs-extra' | 11 | import { remove, ensureDir } from 'fs-extra' |
12 | 12 | ||
@@ -73,6 +73,9 @@ function createDirectoriesIfNotExist () { | |||
73 | tasks.push(ensureDir(dir)) | 73 | tasks.push(ensureDir(dir)) |
74 | } | 74 | } |
75 | 75 | ||
76 | // Playlist directories | ||
77 | tasks.push(ensureDir(HLS_PLAYLIST_DIRECTORY)) | ||
78 | |||
76 | return Promise.all(tasks) | 79 | return Promise.all(tasks) |
77 | } | 80 | } |
78 | 81 | ||
diff --git a/server/initializers/migrations/0330-video-streaming-playlist.ts b/server/initializers/migrations/0330-video-streaming-playlist.ts new file mode 100644 index 000000000..c85a762ab --- /dev/null +++ b/server/initializers/migrations/0330-video-streaming-playlist.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "videoStreamingPlaylist" | ||
12 | ( | ||
13 | "id" SERIAL, | ||
14 | "type" INTEGER NOT NULL, | ||
15 | "playlistUrl" VARCHAR(2000) NOT NULL, | ||
16 | "p2pMediaLoaderInfohashes" VARCHAR(255)[] NOT NULL, | ||
17 | "segmentsSha256Url" VARCHAR(255) NOT NULL, | ||
18 | "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
19 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
20 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
21 | PRIMARY KEY ("id") | ||
22 | );` | ||
23 | await utils.sequelize.query(query) | ||
24 | } | ||
25 | |||
26 | { | ||
27 | const data = { | ||
28 | type: Sequelize.INTEGER, | ||
29 | allowNull: true, | ||
30 | defaultValue: null | ||
31 | } | ||
32 | |||
33 | await utils.queryInterface.changeColumn('videoRedundancy', 'videoFileId', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const query = 'ALTER TABLE "videoRedundancy" ADD COLUMN "videoStreamingPlaylistId" INTEGER NULL ' + | ||
38 | 'REFERENCES "videoStreamingPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE' | ||
39 | |||
40 | await utils.sequelize.query(query) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | function down (options) { | ||
45 | throw new Error('Not implemented.') | ||
46 | } | ||
47 | |||
48 | export { | ||
49 | up, | ||
50 | down | ||
51 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 8215840da..a3f379b76 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -355,10 +355,10 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
355 | 355 | ||
356 | logger.info('Fetching remote actor %s.', actorUrl) | 356 | logger.info('Fetching remote actor %s.', actorUrl) |
357 | 357 | ||
358 | const requestResult = await doRequest(options) | 358 | const requestResult = await doRequest<ActivityPubActor>(options) |
359 | normalizeActor(requestResult.body) | 359 | normalizeActor(requestResult.body) |
360 | 360 | ||
361 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON = requestResult.body |
362 | if (isActorObjectValid(actorJSON) === false) { | 362 | if (isActorObjectValid(actorJSON) === false) { |
363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) | 363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
364 | return { result: undefined, statusCode: requestResult.response.statusCode } | 364 | return { result: undefined, statusCode: requestResult.response.statusCode } |
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index f6f068b45..9a40414bb 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,11 +1,28 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | 1 | import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' |
2 | import { VideoModel } from '../../models/video/video' | 2 | import { VideoModel } from '../../models/video/video' |
3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
4 | import { Transaction } from 'sequelize' | 4 | import { Transaction } from 'sequelize' |
5 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | 6 | ||
6 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { | 7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { |
7 | const url = cacheFileObject.url | ||
8 | 8 | ||
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | |||
15 | return { | ||
16 | expiresOn: new Date(cacheFileObject.expires), | ||
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | ||
24 | |||
25 | const url = cacheFileObject.url | ||
9 | const videoFile = video.VideoFiles.find(f => { | 26 | const videoFile = video.VideoFiles.find(f => { |
10 | return f.resolution === url.height && f.fps === url.fps | 27 | return f.resolution === url.height && f.fps === url.fps |
11 | }) | 28 | }) |
@@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
15 | return { | 32 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: new Date(cacheFileObject.expires), |
17 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
18 | fileUrl: cacheFileObject.url.href, | 35 | fileUrl: url.href, |
19 | strategy: null, | 36 | strategy: null, |
20 | videoFileId: videoFile.id, | 37 | videoFileId: videoFile.id, |
21 | actorId: byActor.id | 38 | actorId: byActor.id |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 73e667ad4..ef20e404c 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -23,17 +23,14 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) { | |||
23 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) | 23 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) |
24 | } | 24 | } |
25 | 25 | ||
26 | async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { | 26 | async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { |
27 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 27 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) |
28 | 28 | ||
29 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id) | ||
30 | const redundancyObject = fileRedundancy.toActivityPubObject() | ||
31 | |||
32 | return sendVideoRelatedCreateActivity({ | 29 | return sendVideoRelatedCreateActivity({ |
33 | byActor, | 30 | byActor, |
34 | video, | 31 | video, |
35 | url: fileRedundancy.url, | 32 | url: fileRedundancy.url, |
36 | object: redundancyObject | 33 | object: fileRedundancy.toActivityPubObject() |
37 | }) | 34 | }) |
38 | } | 35 | } |
39 | 36 | ||
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index eb18a6cb6..ecbf605d6 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -73,7 +73,8 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans | |||
73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { | 73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { |
74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) | 74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) |
75 | 75 | ||
76 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 76 | const videoId = redundancyModel.getVideo().id |
77 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
77 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) | 78 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) |
78 | 79 | ||
79 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) | 80 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index a68f03edf..839f66470 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -61,7 +61,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod | |||
61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { | 61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { |
62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) | 62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) |
63 | 63 | ||
64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) |
65 | 65 | ||
66 | const activityBuilder = (audience: ActivityAudience) => { | 66 | const activityBuilder = (audience: ActivityAudience) => { |
67 | const redundancyObject = redundancyModel.toActivityPubObject() | 67 | const redundancyObject = redundancyModel.toActivityPubObject() |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 38f15448c..4229fe094 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -5,6 +5,8 @@ import { VideoModel } from '../../models/video/video' | |||
5 | import { VideoAbuseModel } from '../../models/video/video-abuse' | 5 | import { VideoAbuseModel } from '../../models/video/video-abuse' |
6 | import { VideoCommentModel } from '../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../models/video/video-comment' |
7 | import { VideoFileModel } from '../../models/video/video-file' | 7 | import { VideoFileModel } from '../../models/video/video-file' |
8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
8 | 10 | ||
9 | function getVideoActivityPubUrl (video: VideoModel) { | 11 | function getVideoActivityPubUrl (video: VideoModel) { |
10 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 12 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
@@ -16,6 +18,10 @@ function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { | |||
16 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` | 18 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` |
17 | } | 19 | } |
18 | 20 | ||
21 | function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
22 | return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}` | ||
23 | } | ||
24 | |||
19 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { | 25 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { |
20 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id | 26 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id |
21 | } | 27 | } |
@@ -92,6 +98,7 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
92 | 98 | ||
93 | export { | 99 | export { |
94 | getVideoActivityPubUrl, | 100 | getVideoActivityPubUrl, |
101 | getVideoCacheStreamingPlaylistActivityPubUrl, | ||
95 | getVideoChannelActivityPubUrl, | 102 | getVideoChannelActivityPubUrl, |
96 | getAccountActivityPubUrl, | 103 | getAccountActivityPubUrl, |
97 | getVideoAbuseActivityPubUrl, | 104 | getVideoAbuseActivityPubUrl, |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index e1e523499..edd01234f 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -2,7 +2,14 @@ import * as Bluebird from 'bluebird' | |||
2 | import * as sequelize from 'sequelize' | 2 | import * as sequelize from 'sequelize' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityUrlObject, | ||
10 | ActivityVideoUrlObject, | ||
11 | VideoState | ||
12 | } from '../../../shared/index' | ||
6 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | 14 | import { VideoPrivacy } from '../../../shared/models/videos' |
8 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 15 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
@@ -30,6 +37,9 @@ import { AccountModel } from '../../models/account/account' | |||
30 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 37 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
31 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 38 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
32 | import { Notifier } from '../notifier' | 39 | import { Notifier } from '../notifier' |
40 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
41 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
42 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
33 | 43 | ||
34 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 44 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
35 | // If the video is not private and published, we federate it | 45 | // If the video is not private and published, we federate it |
@@ -264,6 +274,25 @@ async function updateVideoFromAP (options: { | |||
264 | } | 274 | } |
265 | 275 | ||
266 | { | 276 | { |
277 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) | ||
278 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
279 | |||
280 | // Remove video files that do not exist anymore | ||
281 | const destroyTasks = options.video.VideoStreamingPlaylists | ||
282 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
283 | .map(f => f.destroy(sequelizeOptions)) | ||
284 | await Promise.all(destroyTasks) | ||
285 | |||
286 | // Update or add other one | ||
287 | const upsertTasks = streamingPlaylistAttributes.map(a => { | ||
288 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | ||
289 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
290 | }) | ||
291 | |||
292 | options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) | ||
293 | } | ||
294 | |||
295 | { | ||
267 | // Update Tags | 296 | // Update Tags |
268 | const tags = options.videoObject.tag.map(tag => tag.name) | 297 | const tags = options.videoObject.tag.map(tag => tag.name) |
269 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 298 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
@@ -367,13 +396,25 @@ export { | |||
367 | 396 | ||
368 | // --------------------------------------------------------------------------- | 397 | // --------------------------------------------------------------------------- |
369 | 398 | ||
370 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 399 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
371 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 400 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
372 | 401 | ||
373 | const urlMediaType = url.mediaType || url.mimeType | 402 | const urlMediaType = url.mediaType || url.mimeType |
374 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | 403 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') |
375 | } | 404 | } |
376 | 405 | ||
406 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | ||
407 | const urlMediaType = url.mediaType || url.mimeType | ||
408 | |||
409 | return urlMediaType === 'application/x-mpegURL' | ||
410 | } | ||
411 | |||
412 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
413 | const urlMediaType = tag.mediaType || tag.mimeType | ||
414 | |||
415 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | ||
416 | } | ||
417 | |||
377 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 418 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
378 | logger.debug('Adding remote video %s.', videoObject.id) | 419 | logger.debug('Adding remote video %s.', videoObject.id) |
379 | 420 | ||
@@ -394,8 +435,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
394 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 435 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
395 | await Promise.all(videoFilePromises) | 436 | await Promise.all(videoFilePromises) |
396 | 437 | ||
438 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) | ||
439 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | ||
440 | await Promise.all(playlistPromises) | ||
441 | |||
397 | // Process tags | 442 | // Process tags |
398 | const tags = videoObject.tag.map(t => t.name) | 443 | const tags = videoObject.tag |
444 | .filter(t => t.type === 'Hashtag') | ||
445 | .map(t => t.name) | ||
399 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 446 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
400 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 447 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
401 | 448 | ||
@@ -473,13 +520,13 @@ async function videoActivityObjectToDBAttributes ( | |||
473 | } | 520 | } |
474 | 521 | ||
475 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | 522 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { |
476 | const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] | 523 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
477 | 524 | ||
478 | if (fileUrls.length === 0) { | 525 | if (fileUrls.length === 0) { |
479 | throw new Error('Cannot find video files for ' + video.url) | 526 | throw new Error('Cannot find video files for ' + video.url) |
480 | } | 527 | } |
481 | 528 | ||
482 | const attributes: VideoFileModel[] = [] | 529 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
483 | for (const fileUrl of fileUrls) { | 530 | for (const fileUrl of fileUrls) { |
484 | // Fetch associated magnet uri | 531 | // Fetch associated magnet uri |
485 | const magnet = videoObject.url.find(u => { | 532 | const magnet = videoObject.url.find(u => { |
@@ -502,7 +549,45 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
502 | size: fileUrl.size, | 549 | size: fileUrl.size, |
503 | videoId: video.id, | 550 | videoId: video.id, |
504 | fps: fileUrl.fps || -1 | 551 | fps: fileUrl.fps || -1 |
505 | } as VideoFileModel | 552 | } |
553 | |||
554 | attributes.push(attribute) | ||
555 | } | ||
556 | |||
557 | return attributes | ||
558 | } | ||
559 | |||
560 | function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | ||
561 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
562 | if (playlistUrls.length === 0) return [] | ||
563 | |||
564 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | ||
565 | for (const playlistUrlObject of playlistUrls) { | ||
566 | const p2pMediaLoaderInfohashes = playlistUrlObject.tag | ||
567 | .filter(t => t.type === 'Infohash') | ||
568 | .map(t => t.name) | ||
569 | if (p2pMediaLoaderInfohashes.length === 0) { | ||
570 | logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
571 | continue | ||
572 | } | ||
573 | |||
574 | const segmentsSha256UrlObject = playlistUrlObject.tag | ||
575 | .find(t => { | ||
576 | return isAPPlaylistSegmentHashesUrlObject(t) | ||
577 | }) as ActivityPlaylistSegmentHashesObject | ||
578 | if (!segmentsSha256UrlObject) { | ||
579 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
580 | continue | ||
581 | } | ||
582 | |||
583 | const attribute = { | ||
584 | type: VideoStreamingPlaylistType.HLS, | ||
585 | playlistUrl: playlistUrlObject.href, | ||
586 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
587 | p2pMediaLoaderInfohashes, | ||
588 | videoId: video.id | ||
589 | } | ||
590 | |||
506 | attributes.push(attribute) | 591 | attributes.push(attribute) |
507 | } | 592 | } |
508 | 593 | ||
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index f384a254e..672414cc0 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -296,9 +296,9 @@ class Emailer { | |||
296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
297 | } | 297 | } |
298 | 298 | ||
299 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | 299 | addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { |
300 | const text = `Hi dear user,\n\n` + | 300 | const text = `Hi dear user,\n\n` + |
301 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | 301 | `A reset password procedure for your account ${to} has been requested on ${CONFIG.WEBSERVER.HOST} ` + |
302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | 302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + |
303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + |
304 | `Cheers,\n` + | 304 | `Cheers,\n` + |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts new file mode 100644 index 000000000..3575981f4 --- /dev/null +++ b/server/lib/hls.ts | |||
@@ -0,0 +1,164 @@ | |||
1 | import { VideoModel } from '../models/video/video' | ||
2 | import { basename, join, dirname } from 'path' | ||
3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' | ||
4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | ||
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | ||
6 | import { sha256 } from '../helpers/core-utils' | ||
7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
8 | import { logger } from '../helpers/logger' | ||
9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | ||
10 | import { generateRandomString } from '../helpers/utils' | ||
11 | import { flatten, uniq } from 'lodash' | ||
12 | |||
13 | async function updateMasterHLSPlaylist (video: VideoModel) { | ||
14 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
15 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | ||
16 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
17 | |||
18 | for (const file of video.VideoFiles) { | ||
19 | // If we did not generated a playlist for this resolution, skip | ||
20 | const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
21 | if (await pathExists(filePlaylistPath) === false) continue | ||
22 | |||
23 | const videoFilePath = video.getVideoFilePath(file) | ||
24 | |||
25 | const size = await getVideoFileSize(videoFilePath) | ||
26 | |||
27 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | ||
28 | const resolution = `RESOLUTION=${size.width}x${size.height}` | ||
29 | |||
30 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | ||
31 | if (file.fps) line += ',FRAME-RATE=' + file.fps | ||
32 | |||
33 | masterPlaylists.push(line) | ||
34 | masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
35 | } | ||
36 | |||
37 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | ||
38 | } | ||
39 | |||
40 | async function updateSha256Segments (video: VideoModel) { | ||
41 | const json: { [filename: string]: { [range: string]: string } } = {} | ||
42 | |||
43 | const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
44 | |||
45 | // For all the resolutions available for this video | ||
46 | for (const file of video.VideoFiles) { | ||
47 | const rangeHashes: { [range: string]: string } = {} | ||
48 | |||
49 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | ||
50 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
51 | |||
52 | // Maybe the playlist is not generated for this resolution yet | ||
53 | if (!await pathExists(playlistPath)) continue | ||
54 | |||
55 | const playlistContent = await readFile(playlistPath) | ||
56 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | ||
57 | |||
58 | const fd = await open(videoPath, 'r') | ||
59 | for (const range of ranges) { | ||
60 | const buf = Buffer.alloc(range.length) | ||
61 | await read(fd, buf, 0, range.length, range.offset) | ||
62 | |||
63 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | ||
64 | } | ||
65 | await close(fd) | ||
66 | |||
67 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | ||
68 | json[videoFilename] = rangeHashes | ||
69 | } | ||
70 | |||
71 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
72 | await outputJSON(outputPath, json) | ||
73 | } | ||
74 | |||
75 | function getRangesFromPlaylist (playlistContent: string) { | ||
76 | const ranges: { offset: number, length: number }[] = [] | ||
77 | const lines = playlistContent.split('\n') | ||
78 | const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ | ||
79 | |||
80 | for (const line of lines) { | ||
81 | const captured = regex.exec(line) | ||
82 | |||
83 | if (captured) { | ||
84 | ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | return ranges | ||
89 | } | ||
90 | |||
91 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { | ||
92 | let timer | ||
93 | |||
94 | logger.info('Importing HLS playlist %s', playlistUrl) | ||
95 | |||
96 | return new Promise<string>(async (res, rej) => { | ||
97 | const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10)) | ||
98 | |||
99 | await ensureDir(tmpDirectory) | ||
100 | |||
101 | timer = setTimeout(() => { | ||
102 | deleteTmpDirectory(tmpDirectory) | ||
103 | |||
104 | return rej(new Error('HLS download timeout.')) | ||
105 | }, timeout) | ||
106 | |||
107 | try { | ||
108 | // Fetch master playlist | ||
109 | const subPlaylistUrls = await fetchUniqUrls(playlistUrl) | ||
110 | |||
111 | const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u)) | ||
112 | const fileUrls = uniq(flatten(await Promise.all(subRequests))) | ||
113 | |||
114 | logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls }) | ||
115 | |||
116 | for (const fileUrl of fileUrls) { | ||
117 | const destPath = join(tmpDirectory, basename(fileUrl)) | ||
118 | |||
119 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath) | ||
120 | } | ||
121 | |||
122 | clearTimeout(timer) | ||
123 | |||
124 | await move(tmpDirectory, destinationDir, { overwrite: true }) | ||
125 | |||
126 | return res() | ||
127 | } catch (err) { | ||
128 | deleteTmpDirectory(tmpDirectory) | ||
129 | |||
130 | return rej(err) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | function deleteTmpDirectory (directory: string) { | ||
135 | remove(directory) | ||
136 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | ||
137 | } | ||
138 | |||
139 | async function fetchUniqUrls (playlistUrl: string) { | ||
140 | const { body } = await doRequest<string>({ uri: playlistUrl }) | ||
141 | |||
142 | if (!body) return [] | ||
143 | |||
144 | const urls = body.split('\n') | ||
145 | .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4')) | ||
146 | .map(url => { | ||
147 | if (url.startsWith('http://') || url.startsWith('https://')) return url | ||
148 | |||
149 | return `${dirname(playlistUrl)}/${url}` | ||
150 | }) | ||
151 | |||
152 | return uniq(urls) | ||
153 | } | ||
154 | } | ||
155 | |||
156 | // --------------------------------------------------------------------------- | ||
157 | |||
158 | export { | ||
159 | updateMasterHLSPlaylist, | ||
160 | updateSha256Segments, | ||
161 | downloadPlaylistSegments | ||
162 | } | ||
163 | |||
164 | // --------------------------------------------------------------------------- | ||
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 593e43cc5..04983155c 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -5,17 +5,18 @@ import { VideoModel } from '../../../models/video/video' | |||
5 | import { JobQueue } from '../job-queue' | 5 | import { JobQueue } from '../job-queue' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | 6 | import { federateVideoIfNeeded } from '../../activitypub' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript, CONFIG } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 12 | import { Notifier } from '../../notifier' |
13 | 13 | ||
14 | export type VideoFilePayload = { | 14 | export type VideoFilePayload = { |
15 | videoUUID: string | 15 | videoUUID: string |
16 | isNewVideo?: boolean | ||
17 | resolution?: VideoResolution | 16 | resolution?: VideoResolution |
17 | isNewVideo?: boolean | ||
18 | isPortraitMode?: boolean | 18 | isPortraitMode?: boolean |
19 | generateHlsPlaylist?: boolean | ||
19 | } | 20 | } |
20 | 21 | ||
21 | export type VideoFileImportPayload = { | 22 | export type VideoFileImportPayload = { |
@@ -51,21 +52,38 @@ async function processVideoFile (job: Bull.Job) { | |||
51 | return undefined | 52 | return undefined |
52 | } | 53 | } |
53 | 54 | ||
54 | // Transcoding in other resolution | 55 | if (payload.generateHlsPlaylist) { |
55 | if (payload.resolution) { | 56 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) |
57 | |||
58 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | ||
59 | } else if (payload.resolution) { // Transcoding in other resolution | ||
56 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 60 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
57 | 61 | ||
58 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) | 62 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video, payload) |
59 | } else { | 63 | } else { |
60 | await optimizeVideofile(video) | 64 | await optimizeVideofile(video) |
61 | 65 | ||
62 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) | 66 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload) |
63 | } | 67 | } |
64 | 68 | ||
65 | return video | 69 | return video |
66 | } | 70 | } |
67 | 71 | ||
68 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | 72 | async function onHlsPlaylistGenerationSuccess (video: VideoModel) { |
73 | if (video === undefined) return undefined | ||
74 | |||
75 | await sequelizeTypescript.transaction(async t => { | ||
76 | // Maybe the video changed in database, refresh it | ||
77 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | ||
78 | // Video does not exist anymore | ||
79 | if (!videoDatabase) return undefined | ||
80 | |||
81 | // If the video was not published, we consider it is a new one for other instances | ||
82 | await federateVideoIfNeeded(videoDatabase, false, t) | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel, payload?: VideoFilePayload) { | ||
69 | if (video === undefined) return undefined | 87 | if (video === undefined) return undefined |
70 | 88 | ||
71 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 89 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
@@ -91,13 +109,16 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | |||
91 | return { videoDatabase, videoPublished } | 109 | return { videoDatabase, videoPublished } |
92 | }) | 110 | }) |
93 | 111 | ||
94 | if (videoPublished) { | 112 | // don't notify prior to scheduled video update |
113 | if (videoPublished && !videoDatabase.ScheduleVideoUpdate) { | ||
95 | Notifier.Instance.notifyOnNewVideo(videoDatabase) | 114 | Notifier.Instance.notifyOnNewVideo(videoDatabase) |
96 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | 115 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) |
97 | } | 116 | } |
117 | |||
118 | await createHlsJobIfEnabled(payload) | ||
98 | } | 119 | } |
99 | 120 | ||
100 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { | 121 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoFilePayload) { |
101 | if (videoArg === undefined) return undefined | 122 | if (videoArg === undefined) return undefined |
102 | 123 | ||
103 | // Outside the transaction (IO on disk) | 124 | // Outside the transaction (IO on disk) |
@@ -144,13 +165,18 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo | |||
144 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) | 165 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) |
145 | } | 166 | } |
146 | 167 | ||
147 | await federateVideoIfNeeded(videoDatabase, isNewVideo, t) | 168 | await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t) |
148 | 169 | ||
149 | return { videoDatabase, videoPublished } | 170 | return { videoDatabase, videoPublished } |
150 | }) | 171 | }) |
151 | 172 | ||
152 | if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | 173 | // don't notify prior to scheduled video update |
153 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | 174 | if (!videoDatabase.ScheduleVideoUpdate) { |
175 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
176 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
177 | } | ||
178 | |||
179 | await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) | ||
154 | } | 180 | } |
155 | 181 | ||
156 | // --------------------------------------------------------------------------- | 182 | // --------------------------------------------------------------------------- |
@@ -159,3 +185,20 @@ export { | |||
159 | processVideoFile, | 185 | processVideoFile, |
160 | processVideoFileImport | 186 | processVideoFileImport |
161 | } | 187 | } |
188 | |||
189 | // --------------------------------------------------------------------------- | ||
190 | |||
191 | function createHlsJobIfEnabled (payload?: VideoFilePayload) { | ||
192 | // Generate HLS playlist? | ||
193 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { | ||
194 | const hlsTranscodingPayload = { | ||
195 | videoUUID: payload.videoUUID, | ||
196 | resolution: payload.resolution, | ||
197 | isPortraitMode: payload.isPortraitMode, | ||
198 | |||
199 | generateHlsPlaylist: true | ||
200 | } | ||
201 | |||
202 | return JobQueue.Instance.createJob({ type: 'video-file', payload: hlsTranscodingPayload }) | ||
203 | } | ||
204 | } | ||
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index f643ee226..1a48f2bd0 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { AbstractScheduler } from './abstract-scheduler' |
2 | import { CONFIG, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' | 2 | import { CONFIG, HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
@@ -9,9 +9,19 @@ import { join } from 'path' | |||
9 | import { move } from 'fs-extra' | 9 | import { move } from 'fs-extra' |
10 | import { getServerActor } from '../../helpers/utils' | 10 | import { getServerActor } from '../../helpers/utils' |
11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
12 | import { getVideoCacheFileActivityPubUrl } from '../activitypub/url' | 12 | import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
13 | import { removeVideoRedundancy } from '../redundancy' | 13 | import { removeVideoRedundancy } from '../redundancy' |
14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' | 14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
16 | import { VideoModel } from '../../models/video/video' | ||
17 | import { downloadPlaylistSegments } from '../hls' | ||
18 | |||
19 | type CandidateToDuplicate = { | ||
20 | redundancy: VideosRedundancy, | ||
21 | video: VideoModel, | ||
22 | files: VideoFileModel[], | ||
23 | streamingPlaylists: VideoStreamingPlaylistModel[] | ||
24 | } | ||
15 | 25 | ||
16 | export class VideosRedundancyScheduler extends AbstractScheduler { | 26 | export class VideosRedundancyScheduler extends AbstractScheduler { |
17 | 27 | ||
@@ -24,28 +34,32 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
24 | } | 34 | } |
25 | 35 | ||
26 | protected async internalExecute () { | 36 | protected async internalExecute () { |
27 | for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | 37 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
28 | logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) | 38 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) |
29 | 39 | ||
30 | try { | 40 | try { |
31 | const videoToDuplicate = await this.findVideoToDuplicate(obj) | 41 | const videoToDuplicate = await this.findVideoToDuplicate(redundancyConfig) |
32 | if (!videoToDuplicate) continue | 42 | if (!videoToDuplicate) continue |
33 | 43 | ||
34 | const videoFiles = videoToDuplicate.VideoFiles | 44 | const candidateToDuplicate = { |
35 | videoFiles.forEach(f => f.Video = videoToDuplicate) | 45 | video: videoToDuplicate, |
46 | redundancy: redundancyConfig, | ||
47 | files: videoToDuplicate.VideoFiles, | ||
48 | streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists | ||
49 | } | ||
36 | 50 | ||
37 | await this.purgeCacheIfNeeded(obj, videoFiles) | 51 | await this.purgeCacheIfNeeded(candidateToDuplicate) |
38 | 52 | ||
39 | if (await this.isTooHeavy(obj, videoFiles)) { | 53 | if (await this.isTooHeavy(candidateToDuplicate)) { |
40 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) | 54 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) |
41 | continue | 55 | continue |
42 | } | 56 | } |
43 | 57 | ||
44 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, obj.strategy) | 58 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, redundancyConfig.strategy) |
45 | 59 | ||
46 | await this.createVideoRedundancy(obj, videoFiles) | 60 | await this.createVideoRedundancies(candidateToDuplicate) |
47 | } catch (err) { | 61 | } catch (err) { |
48 | logger.error('Cannot run videos redundancy %s.', obj.strategy, { err }) | 62 | logger.error('Cannot run videos redundancy %s.', redundancyConfig.strategy, { err }) |
49 | } | 63 | } |
50 | } | 64 | } |
51 | 65 | ||
@@ -63,25 +77,35 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
63 | 77 | ||
64 | for (const redundancyModel of expired) { | 78 | for (const redundancyModel of expired) { |
65 | try { | 79 | try { |
66 | await this.extendsOrDeleteRedundancy(redundancyModel) | 80 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
81 | const candidate = { | ||
82 | redundancy: redundancyConfig, | ||
83 | video: null, | ||
84 | files: [], | ||
85 | streamingPlaylists: [] | ||
86 | } | ||
87 | |||
88 | // If the administrator disabled the redundancy or decreased the cache size, remove this redundancy instead of extending it | ||
89 | if (!redundancyConfig || await this.isTooHeavy(candidate)) { | ||
90 | logger.info('Destroying redundancy %s because the cache size %s is too heavy.', redundancyModel.url, redundancyModel.strategy) | ||
91 | await removeVideoRedundancy(redundancyModel) | ||
92 | } else { | ||
93 | await this.extendsRedundancy(redundancyModel) | ||
94 | } | ||
67 | } catch (err) { | 95 | } catch (err) { |
68 | logger.error('Cannot extend expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel)) | 96 | logger.error( |
97 | 'Cannot extend or remove expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel), | ||
98 | { err } | ||
99 | ) | ||
69 | } | 100 | } |
70 | } | 101 | } |
71 | } | 102 | } |
72 | 103 | ||
73 | private async extendsOrDeleteRedundancy (redundancyModel: VideoRedundancyModel) { | 104 | private async extendsRedundancy (redundancyModel: VideoRedundancyModel) { |
74 | // Refresh the video, maybe it was deleted | ||
75 | const video = await this.loadAndRefreshVideo(redundancyModel.VideoFile.Video.url) | ||
76 | |||
77 | if (!video) { | ||
78 | logger.info('Destroying existing redundancy %s, because the associated video does not exist anymore.', redundancyModel.url) | ||
79 | |||
80 | await redundancyModel.destroy() | ||
81 | return | ||
82 | } | ||
83 | |||
84 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) | 105 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
106 | // Redundancy strategy disabled, remove our redundancy instead of extending expiration | ||
107 | if (!redundancy) await removeVideoRedundancy(redundancyModel) | ||
108 | |||
85 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) | 109 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) |
86 | } | 110 | } |
87 | 111 | ||
@@ -112,49 +136,93 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
112 | } | 136 | } |
113 | } | 137 | } |
114 | 138 | ||
115 | private async createVideoRedundancy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 139 | private async createVideoRedundancies (data: CandidateToDuplicate) { |
116 | const serverActor = await getServerActor() | 140 | const video = await this.loadAndRefreshVideo(data.video.url) |
141 | |||
142 | if (!video) { | ||
143 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', data.video.url) | ||
117 | 144 | ||
118 | for (const file of filesToDuplicate) { | 145 | return |
119 | const video = await this.loadAndRefreshVideo(file.Video.url) | 146 | } |
120 | 147 | ||
148 | for (const file of data.files) { | ||
121 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) | 149 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) |
122 | if (existingRedundancy) { | 150 | if (existingRedundancy) { |
123 | await this.extendsOrDeleteRedundancy(existingRedundancy) | 151 | await this.extendsRedundancy(existingRedundancy) |
124 | 152 | ||
125 | continue | 153 | continue |
126 | } | 154 | } |
127 | 155 | ||
128 | if (!video) { | 156 | await this.createVideoFileRedundancy(data.redundancy, video, file) |
129 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', file.Video.url) | 157 | } |
158 | |||
159 | for (const streamingPlaylist of data.streamingPlaylists) { | ||
160 | const existingRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(streamingPlaylist.id) | ||
161 | if (existingRedundancy) { | ||
162 | await this.extendsRedundancy(existingRedundancy) | ||
130 | 163 | ||
131 | continue | 164 | continue |
132 | } | 165 | } |
133 | 166 | ||
134 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 167 | await this.createStreamingPlaylistRedundancy(data.redundancy, video, streamingPlaylist) |
168 | } | ||
169 | } | ||
135 | 170 | ||
136 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 171 | private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) { |
137 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) | 172 | file.Video = video |
138 | 173 | ||
139 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 174 | const serverActor = await getServerActor() |
140 | 175 | ||
141 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) | 176 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) |
142 | await move(tmpPath, destPath) | ||
143 | 177 | ||
144 | const createdModel = await VideoRedundancyModel.create({ | 178 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
145 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 179 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) |
146 | url: getVideoCacheFileActivityPubUrl(file), | ||
147 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), | ||
148 | strategy: redundancy.strategy, | ||
149 | videoFileId: file.id, | ||
150 | actorId: serverActor.id | ||
151 | }) | ||
152 | createdModel.VideoFile = file | ||
153 | 180 | ||
154 | await sendCreateCacheFile(serverActor, createdModel) | 181 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
155 | 182 | ||
156 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | 183 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) |
157 | } | 184 | await move(tmpPath, destPath) |
185 | |||
186 | const createdModel = await VideoRedundancyModel.create({ | ||
187 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
188 | url: getVideoCacheFileActivityPubUrl(file), | ||
189 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), | ||
190 | strategy: redundancy.strategy, | ||
191 | videoFileId: file.id, | ||
192 | actorId: serverActor.id | ||
193 | }) | ||
194 | |||
195 | createdModel.VideoFile = file | ||
196 | |||
197 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
198 | |||
199 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | ||
200 | } | ||
201 | |||
202 | private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
203 | playlist.Video = video | ||
204 | |||
205 | const serverActor = await getServerActor() | ||
206 | |||
207 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) | ||
208 | |||
209 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | ||
210 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | ||
211 | |||
212 | const createdModel = await VideoRedundancyModel.create({ | ||
213 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
214 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), | ||
215 | fileUrl: playlist.getVideoRedundancyUrl(CONFIG.WEBSERVER.URL), | ||
216 | strategy: redundancy.strategy, | ||
217 | videoStreamingPlaylistId: playlist.id, | ||
218 | actorId: serverActor.id | ||
219 | }) | ||
220 | |||
221 | createdModel.VideoStreamingPlaylist = playlist | ||
222 | |||
223 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
224 | |||
225 | logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) | ||
158 | } | 226 | } |
159 | 227 | ||
160 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { | 228 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { |
@@ -168,8 +236,9 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
168 | await sendUpdateCacheFile(serverActor, redundancy) | 236 | await sendUpdateCacheFile(serverActor, redundancy) |
169 | } | 237 | } |
170 | 238 | ||
171 | private async purgeCacheIfNeeded (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 239 | private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) { |
172 | while (this.isTooHeavy(redundancy, filesToDuplicate)) { | 240 | while (this.isTooHeavy(candidateToDuplicate)) { |
241 | const redundancy = candidateToDuplicate.redundancy | ||
173 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) | 242 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) |
174 | if (!toDelete) return | 243 | if (!toDelete) return |
175 | 244 | ||
@@ -177,11 +246,11 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
177 | } | 246 | } |
178 | } | 247 | } |
179 | 248 | ||
180 | private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 249 | private async isTooHeavy (candidateToDuplicate: CandidateToDuplicate) { |
181 | const maxSize = redundancy.size | 250 | const maxSize = candidateToDuplicate.redundancy.size |
182 | 251 | ||
183 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) | 252 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(candidateToDuplicate.redundancy.strategy) |
184 | const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate) | 253 | const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(candidateToDuplicate.files, candidateToDuplicate.streamingPlaylists) |
185 | 254 | ||
186 | return totalWillDuplicate > maxSize | 255 | return totalWillDuplicate > maxSize |
187 | } | 256 | } |
@@ -191,13 +260,15 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
191 | } | 260 | } |
192 | 261 | ||
193 | private buildEntryLogId (object: VideoRedundancyModel) { | 262 | private buildEntryLogId (object: VideoRedundancyModel) { |
194 | return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` | 263 | if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` |
264 | |||
265 | return `${object.VideoStreamingPlaylist.playlistUrl}` | ||
195 | } | 266 | } |
196 | 267 | ||
197 | private getTotalFileSizes (files: VideoFileModel[]) { | 268 | private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) { |
198 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size | 269 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size |
199 | 270 | ||
200 | return files.reduce(fileReducer, 0) | 271 | return files.reduce(fileReducer, 0) * playlists.length |
201 | } | 272 | } |
202 | 273 | ||
203 | private async loadAndRefreshVideo (videoUrl: string) { | 274 | private async loadAndRefreshVideo (videoUrl: string) { |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 4460f46e4..086b860a2 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,11 +1,14 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' |
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, move, stat } from 'fs-extra' | 4 | import { copy, ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | import { updateMasterHLSPlaylist, updateSha256Segments } from './hls' | ||
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
11 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | ||
9 | 12 | ||
10 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { | 13 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { |
11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 14 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
@@ -17,7 +20,8 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
17 | 20 | ||
18 | const transcodeOptions = { | 21 | const transcodeOptions = { |
19 | inputPath: videoInputPath, | 22 | inputPath: videoInputPath, |
20 | outputPath: videoTranscodedPath | 23 | outputPath: videoTranscodedPath, |
24 | resolution: inputVideoFile.resolution | ||
21 | } | 25 | } |
22 | 26 | ||
23 | // Could be very long! | 27 | // Could be very long! |
@@ -47,7 +51,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
47 | } | 51 | } |
48 | } | 52 | } |
49 | 53 | ||
50 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | 54 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { |
51 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 55 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
52 | const extname = '.mp4' | 56 | const extname = '.mp4' |
53 | 57 | ||
@@ -60,13 +64,13 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
60 | size: 0, | 64 | size: 0, |
61 | videoId: video.id | 65 | videoId: video.id |
62 | }) | 66 | }) |
63 | const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile)) | 67 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) |
64 | 68 | ||
65 | const transcodeOptions = { | 69 | const transcodeOptions = { |
66 | inputPath: videoInputPath, | 70 | inputPath: videoInputPath, |
67 | outputPath: videoOutputPath, | 71 | outputPath: videoOutputPath, |
68 | resolution, | 72 | resolution, |
69 | isPortraitMode | 73 | isPortraitMode: isPortrait |
70 | } | 74 | } |
71 | 75 | ||
72 | await transcode(transcodeOptions) | 76 | await transcode(transcodeOptions) |
@@ -84,6 +88,41 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
84 | video.VideoFiles.push(newVideoFile) | 88 | video.VideoFiles.push(newVideoFile) |
85 | } | 89 | } |
86 | 90 | ||
91 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | ||
92 | const baseHlsDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
93 | await ensureDir(join(HLS_PLAYLIST_DIRECTORY, video.uuid)) | ||
94 | |||
95 | const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getOriginalFile())) | ||
96 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
97 | |||
98 | const transcodeOptions = { | ||
99 | inputPath: videoInputPath, | ||
100 | outputPath, | ||
101 | resolution, | ||
102 | isPortraitMode, | ||
103 | |||
104 | hlsPlaylist: { | ||
105 | videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | await transcode(transcodeOptions) | ||
110 | |||
111 | await updateMasterHLSPlaylist(video) | ||
112 | await updateSha256Segments(video) | ||
113 | |||
114 | const playlistUrl = CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
115 | |||
116 | await VideoStreamingPlaylistModel.upsert({ | ||
117 | videoId: video.id, | ||
118 | playlistUrl, | ||
119 | segmentsSha256Url: CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid), | ||
120 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles), | ||
121 | |||
122 | type: VideoStreamingPlaylistType.HLS | ||
123 | }) | ||
124 | } | ||
125 | |||
87 | async function importVideoFile (video: VideoModel, inputFilePath: string) { | 126 | async function importVideoFile (video: VideoModel, inputFilePath: string) { |
88 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | 127 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) |
89 | const { size } = await stat(inputFilePath) | 128 | const { size } = await stat(inputFilePath) |
@@ -125,6 +164,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) { | |||
125 | } | 164 | } |
126 | 165 | ||
127 | export { | 166 | export { |
167 | generateHlsPlaylist, | ||
128 | optimizeVideofile, | 168 | optimizeVideofile, |
129 | transcodeOriginalVideofile, | 169 | transcodeOriginalVideofile, |
130 | importVideoFile | 170 | importVideoFile |
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts index 8b919af0d..5fa9d1ab5 100644 --- a/server/middlewares/csp.ts +++ b/server/middlewares/csp.ts | |||
@@ -16,7 +16,7 @@ const baseDirectives = Object.assign({}, | |||
16 | baseUri: ["'self'"], | 16 | baseUri: ["'self'"], |
17 | manifestSrc: ["'self'"], | 17 | manifestSrc: ["'self'"], |
18 | frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed | 18 | frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed |
19 | workerSrc: ["'self'"] // instead of deprecated child-src | 19 | workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src |
20 | }, | 20 | }, |
21 | CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, | 21 | CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, |
22 | CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} | 22 | CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} |
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c72ab78b2..329322509 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -13,7 +13,7 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow' | |||
13 | import { SERVER_ACTOR_NAME } from '../../initializers' | 13 | import { SERVER_ACTOR_NAME } from '../../initializers' |
14 | import { ServerModel } from '../../models/server/server' | 14 | import { ServerModel } from '../../models/server/server' |
15 | 15 | ||
16 | const videoRedundancyGetValidator = [ | 16 | const videoFileRedundancyGetValidator = [ |
17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
18 | param('resolution') | 18 | param('resolution') |
19 | .customSanitizer(toIntOrNull) | 19 | .customSanitizer(toIntOrNull) |
@@ -24,7 +24,7 @@ const videoRedundancyGetValidator = [ | |||
24 | .custom(exists).withMessage('Should have a valid fps'), | 24 | .custom(exists).withMessage('Should have a valid fps'), |
25 | 25 | ||
26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) | 27 | logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params }) |
28 | 28 | ||
29 | if (areValidationErrors(req, res)) return | 29 | if (areValidationErrors(req, res)) return |
30 | if (!await isVideoExist(req.params.videoId, res)) return | 30 | if (!await isVideoExist(req.params.videoId, res)) return |
@@ -38,7 +38,31 @@ const videoRedundancyGetValidator = [ | |||
38 | res.locals.videoFile = videoFile | 38 | res.locals.videoFile = videoFile |
39 | 39 | ||
40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) |
41 | if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) | 41 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) |
42 | res.locals.videoRedundancy = videoRedundancy | ||
43 | |||
44 | return next() | ||
45 | } | ||
46 | ] | ||
47 | |||
48 | const videoPlaylistRedundancyGetValidator = [ | ||
49 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | ||
50 | param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'), | ||
51 | |||
52 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
53 | logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params }) | ||
54 | |||
55 | if (areValidationErrors(req, res)) return | ||
56 | if (!await isVideoExist(req.params.videoId, res)) return | ||
57 | |||
58 | const video: VideoModel = res.locals.video | ||
59 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType) | ||
60 | |||
61 | if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' }) | ||
62 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist | ||
63 | |||
64 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) | ||
65 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) | ||
42 | res.locals.videoRedundancy = videoRedundancy | 66 | res.locals.videoRedundancy = videoRedundancy |
43 | 67 | ||
44 | return next() | 68 | return next() |
@@ -75,6 +99,7 @@ const updateServerRedundancyValidator = [ | |||
75 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
76 | 100 | ||
77 | export { | 101 | export { |
78 | videoRedundancyGetValidator, | 102 | videoFileRedundancyGetValidator, |
103 | videoPlaylistRedundancyGetValidator, | ||
79 | updateServerRedundancyValidator | 104 | updateServerRedundancyValidator |
80 | } | 105 | } |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 1bb0bfb1b..a52e3060a 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -113,6 +113,7 @@ const deleteMeValidator = [ | |||
113 | 113 | ||
114 | const usersUpdateValidator = [ | 114 | const usersUpdateValidator = [ |
115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
116 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | ||
116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 117 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
117 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), | 118 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), |
118 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 119 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
@@ -233,6 +234,7 @@ const usersAskResetPasswordValidator = [ | |||
233 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) | 234 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) |
234 | 235 | ||
235 | if (areValidationErrors(req, res)) return | 236 | if (areValidationErrors(req, res)) return |
237 | |||
236 | const exists = await checkUserEmailExist(req.body.email, res, false) | 238 | const exists = await checkUserEmailExist(req.body.email, res, false) |
237 | if (!exists) { | 239 | if (!exists) { |
238 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) | 240 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 9e4f982a3..6cdbb827b 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -27,11 +27,33 @@ import { VideoBlacklistModel } from '../video/video-blacklist' | |||
27 | import { VideoImportModel } from '../video/video-import' | 27 | import { VideoImportModel } from '../video/video-import' |
28 | import { ActorModel } from '../activitypub/actor' | 28 | import { ActorModel } from '../activitypub/actor' |
29 | import { ActorFollowModel } from '../activitypub/actor-follow' | 29 | import { ActorFollowModel } from '../activitypub/actor-follow' |
30 | import { AvatarModel } from '../avatar/avatar' | ||
31 | import { ServerModel } from '../server/server' | ||
30 | 32 | ||
31 | enum ScopeNames { | 33 | enum ScopeNames { |
32 | WITH_ALL = 'WITH_ALL' | 34 | WITH_ALL = 'WITH_ALL' |
33 | } | 35 | } |
34 | 36 | ||
37 | function buildActorWithAvatarInclude () { | ||
38 | return { | ||
39 | attributes: [ 'preferredUsername' ], | ||
40 | model: () => ActorModel.unscoped(), | ||
41 | required: true, | ||
42 | include: [ | ||
43 | { | ||
44 | attributes: [ 'filename' ], | ||
45 | model: () => AvatarModel.unscoped(), | ||
46 | required: false | ||
47 | }, | ||
48 | { | ||
49 | attributes: [ 'host' ], | ||
50 | model: () => ServerModel.unscoped(), | ||
51 | required: false | ||
52 | } | ||
53 | ] | ||
54 | } | ||
55 | } | ||
56 | |||
35 | function buildVideoInclude (required: boolean) { | 57 | function buildVideoInclude (required: boolean) { |
36 | return { | 58 | return { |
37 | attributes: [ 'id', 'uuid', 'name' ], | 59 | attributes: [ 'id', 'uuid', 'name' ], |
@@ -40,19 +62,21 @@ function buildVideoInclude (required: boolean) { | |||
40 | } | 62 | } |
41 | } | 63 | } |
42 | 64 | ||
43 | function buildChannelInclude (required: boolean) { | 65 | function buildChannelInclude (required: boolean, withActor = false) { |
44 | return { | 66 | return { |
45 | required, | 67 | required, |
46 | attributes: [ 'id', 'name' ], | 68 | attributes: [ 'id', 'name' ], |
47 | model: () => VideoChannelModel.unscoped() | 69 | model: () => VideoChannelModel.unscoped(), |
70 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
48 | } | 71 | } |
49 | } | 72 | } |
50 | 73 | ||
51 | function buildAccountInclude (required: boolean) { | 74 | function buildAccountInclude (required: boolean, withActor = false) { |
52 | return { | 75 | return { |
53 | required, | 76 | required, |
54 | attributes: [ 'id', 'name' ], | 77 | attributes: [ 'id', 'name' ], |
55 | model: () => AccountModel.unscoped() | 78 | model: () => AccountModel.unscoped(), |
79 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
56 | } | 80 | } |
57 | } | 81 | } |
58 | 82 | ||
@@ -60,47 +84,40 @@ function buildAccountInclude (required: boolean) { | |||
60 | [ScopeNames.WITH_ALL]: { | 84 | [ScopeNames.WITH_ALL]: { |
61 | include: [ | 85 | include: [ |
62 | Object.assign(buildVideoInclude(false), { | 86 | Object.assign(buildVideoInclude(false), { |
63 | include: [ buildChannelInclude(true) ] | 87 | include: [ buildChannelInclude(true, true) ] |
64 | }), | 88 | }), |
89 | |||
65 | { | 90 | { |
66 | attributes: [ 'id', 'originCommentId' ], | 91 | attributes: [ 'id', 'originCommentId' ], |
67 | model: () => VideoCommentModel.unscoped(), | 92 | model: () => VideoCommentModel.unscoped(), |
68 | required: false, | 93 | required: false, |
69 | include: [ | 94 | include: [ |
70 | buildAccountInclude(true), | 95 | buildAccountInclude(true, true), |
71 | buildVideoInclude(true) | 96 | buildVideoInclude(true) |
72 | ] | 97 | ] |
73 | }, | 98 | }, |
99 | |||
74 | { | 100 | { |
75 | attributes: [ 'id' ], | 101 | attributes: [ 'id' ], |
76 | model: () => VideoAbuseModel.unscoped(), | 102 | model: () => VideoAbuseModel.unscoped(), |
77 | required: false, | 103 | required: false, |
78 | include: [ buildVideoInclude(true) ] | 104 | include: [ buildVideoInclude(true) ] |
79 | }, | 105 | }, |
106 | |||
80 | { | 107 | { |
81 | attributes: [ 'id' ], | 108 | attributes: [ 'id' ], |
82 | model: () => VideoBlacklistModel.unscoped(), | 109 | model: () => VideoBlacklistModel.unscoped(), |
83 | required: false, | 110 | required: false, |
84 | include: [ buildVideoInclude(true) ] | 111 | include: [ buildVideoInclude(true) ] |
85 | }, | 112 | }, |
113 | |||
86 | { | 114 | { |
87 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | 115 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], |
88 | model: () => VideoImportModel.unscoped(), | 116 | model: () => VideoImportModel.unscoped(), |
89 | required: false, | 117 | required: false, |
90 | include: [ buildVideoInclude(false) ] | 118 | include: [ buildVideoInclude(false) ] |
91 | }, | 119 | }, |
92 | { | 120 | |
93 | attributes: [ 'id', 'name' ], | ||
94 | model: () => AccountModel.unscoped(), | ||
95 | required: false, | ||
96 | include: [ | ||
97 | { | ||
98 | attributes: [ 'id', 'preferredUsername' ], | ||
99 | model: () => ActorModel.unscoped(), | ||
100 | required: true | ||
101 | } | ||
102 | ] | ||
103 | }, | ||
104 | { | 121 | { |
105 | attributes: [ 'id' ], | 122 | attributes: [ 'id' ], |
106 | model: () => ActorFollowModel.unscoped(), | 123 | model: () => ActorFollowModel.unscoped(), |
@@ -111,7 +128,23 @@ function buildAccountInclude (required: boolean) { | |||
111 | model: () => ActorModel.unscoped(), | 128 | model: () => ActorModel.unscoped(), |
112 | required: true, | 129 | required: true, |
113 | as: 'ActorFollower', | 130 | as: 'ActorFollower', |
114 | include: [ buildAccountInclude(true) ] | 131 | include: [ |
132 | { | ||
133 | attributes: [ 'id', 'name' ], | ||
134 | model: () => AccountModel.unscoped(), | ||
135 | required: true | ||
136 | }, | ||
137 | { | ||
138 | attributes: [ 'filename' ], | ||
139 | model: () => AvatarModel.unscoped(), | ||
140 | required: false | ||
141 | }, | ||
142 | { | ||
143 | attributes: [ 'host' ], | ||
144 | model: () => ServerModel.unscoped(), | ||
145 | required: false | ||
146 | } | ||
147 | ] | ||
115 | }, | 148 | }, |
116 | { | 149 | { |
117 | attributes: [ 'preferredUsername' ], | 150 | attributes: [ 'preferredUsername' ], |
@@ -124,7 +157,9 @@ function buildAccountInclude (required: boolean) { | |||
124 | ] | 157 | ] |
125 | } | 158 | } |
126 | ] | 159 | ] |
127 | } | 160 | }, |
161 | |||
162 | buildAccountInclude(false, true) | ||
128 | ] | 163 | ] |
129 | } | 164 | } |
130 | }) | 165 | }) |
@@ -132,10 +167,63 @@ function buildAccountInclude (required: boolean) { | |||
132 | tableName: 'userNotification', | 167 | tableName: 'userNotification', |
133 | indexes: [ | 168 | indexes: [ |
134 | { | 169 | { |
135 | fields: [ 'videoId' ] | 170 | fields: [ 'userId' ] |
171 | }, | ||
172 | { | ||
173 | fields: [ 'videoId' ], | ||
174 | where: { | ||
175 | videoId: { | ||
176 | [Op.ne]: null | ||
177 | } | ||
178 | } | ||
136 | }, | 179 | }, |
137 | { | 180 | { |
138 | fields: [ 'commentId' ] | 181 | fields: [ 'commentId' ], |
182 | where: { | ||
183 | commentId: { | ||
184 | [Op.ne]: null | ||
185 | } | ||
186 | } | ||
187 | }, | ||
188 | { | ||
189 | fields: [ 'videoAbuseId' ], | ||
190 | where: { | ||
191 | videoAbuseId: { | ||
192 | [Op.ne]: null | ||
193 | } | ||
194 | } | ||
195 | }, | ||
196 | { | ||
197 | fields: [ 'videoBlacklistId' ], | ||
198 | where: { | ||
199 | videoBlacklistId: { | ||
200 | [Op.ne]: null | ||
201 | } | ||
202 | } | ||
203 | }, | ||
204 | { | ||
205 | fields: [ 'videoImportId' ], | ||
206 | where: { | ||
207 | videoImportId: { | ||
208 | [Op.ne]: null | ||
209 | } | ||
210 | } | ||
211 | }, | ||
212 | { | ||
213 | fields: [ 'accountId' ], | ||
214 | where: { | ||
215 | accountId: { | ||
216 | [Op.ne]: null | ||
217 | } | ||
218 | } | ||
219 | }, | ||
220 | { | ||
221 | fields: [ 'actorFollowId' ], | ||
222 | where: { | ||
223 | actorFollowId: { | ||
224 | [Op.ne]: null | ||
225 | } | ||
226 | } | ||
139 | } | 227 | } |
140 | ] | 228 | ] |
141 | }) | 229 | }) |
@@ -297,12 +385,9 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
297 | } | 385 | } |
298 | 386 | ||
299 | toFormattedJSON (): UserNotification { | 387 | toFormattedJSON (): UserNotification { |
300 | const video = this.Video ? Object.assign(this.formatVideo(this.Video), { | 388 | const video = this.Video |
301 | channel: { | 389 | ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) |
302 | id: this.Video.VideoChannel.id, | 390 | : undefined |
303 | displayName: this.Video.VideoChannel.getDisplayName() | ||
304 | } | ||
305 | }) : undefined | ||
306 | 391 | ||
307 | const videoImport = this.VideoImport ? { | 392 | const videoImport = this.VideoImport ? { |
308 | id: this.VideoImport.id, | 393 | id: this.VideoImport.id, |
@@ -315,10 +400,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
315 | const comment = this.Comment ? { | 400 | const comment = this.Comment ? { |
316 | id: this.Comment.id, | 401 | id: this.Comment.id, |
317 | threadId: this.Comment.getThreadId(), | 402 | threadId: this.Comment.getThreadId(), |
318 | account: { | 403 | account: this.formatActor(this.Comment.Account), |
319 | id: this.Comment.Account.id, | ||
320 | displayName: this.Comment.Account.getDisplayName() | ||
321 | }, | ||
322 | video: this.formatVideo(this.Comment.Video) | 404 | video: this.formatVideo(this.Comment.Video) |
323 | } : undefined | 405 | } : undefined |
324 | 406 | ||
@@ -332,17 +414,16 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
332 | video: this.formatVideo(this.VideoBlacklist.Video) | 414 | video: this.formatVideo(this.VideoBlacklist.Video) |
333 | } : undefined | 415 | } : undefined |
334 | 416 | ||
335 | const account = this.Account ? { | 417 | const account = this.Account ? this.formatActor(this.Account) : undefined |
336 | id: this.Account.id, | ||
337 | displayName: this.Account.getDisplayName(), | ||
338 | name: this.Account.Actor.preferredUsername | ||
339 | } : undefined | ||
340 | 418 | ||
341 | const actorFollow = this.ActorFollow ? { | 419 | const actorFollow = this.ActorFollow ? { |
342 | id: this.ActorFollow.id, | 420 | id: this.ActorFollow.id, |
343 | follower: { | 421 | follower: { |
422 | id: this.ActorFollow.ActorFollower.Account.id, | ||
344 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | 423 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), |
345 | name: this.ActorFollow.ActorFollower.preferredUsername | 424 | name: this.ActorFollow.ActorFollower.preferredUsername, |
425 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined, | ||
426 | host: this.ActorFollow.ActorFollower.getHost() | ||
346 | }, | 427 | }, |
347 | following: { | 428 | following: { |
348 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', | 429 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', |
@@ -374,4 +455,18 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
374 | name: video.name | 455 | name: video.name |
375 | } | 456 | } |
376 | } | 457 | } |
458 | |||
459 | private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { | ||
460 | const avatar = accountOrChannel.Actor.Avatar | ||
461 | ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() } | ||
462 | : undefined | ||
463 | |||
464 | return { | ||
465 | id: accountOrChannel.id, | ||
466 | displayName: accountOrChannel.getDisplayName(), | ||
467 | name: accountOrChannel.Actor.preferredUsername, | ||
468 | host: accountOrChannel.Actor.getHost(), | ||
469 | avatar | ||
470 | } | ||
471 | } | ||
377 | } | 472 | } |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8f2ef2d9a..b722bed14 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -28,6 +28,7 @@ import { sample } from 'lodash' | |||
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import * as Sequelize from 'sequelize' | 30 | import * as Sequelize from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | ||
31 | 32 | ||
32 | export enum ScopeNames { | 33 | export enum ScopeNames { |
33 | WITH_VIDEO = 'WITH_VIDEO' | 34 | WITH_VIDEO = 'WITH_VIDEO' |
@@ -38,7 +39,17 @@ export enum ScopeNames { | |||
38 | include: [ | 39 | include: [ |
39 | { | 40 | { |
40 | model: () => VideoFileModel, | 41 | model: () => VideoFileModel, |
41 | required: true, | 42 | required: false, |
43 | include: [ | ||
44 | { | ||
45 | model: () => VideoModel, | ||
46 | required: true | ||
47 | } | ||
48 | ] | ||
49 | }, | ||
50 | { | ||
51 | model: () => VideoStreamingPlaylistModel, | ||
52 | required: false, | ||
42 | include: [ | 53 | include: [ |
43 | { | 54 | { |
44 | model: () => VideoModel, | 55 | model: () => VideoModel, |
@@ -97,12 +108,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
97 | 108 | ||
98 | @BelongsTo(() => VideoFileModel, { | 109 | @BelongsTo(() => VideoFileModel, { |
99 | foreignKey: { | 110 | foreignKey: { |
100 | allowNull: false | 111 | allowNull: true |
101 | }, | 112 | }, |
102 | onDelete: 'cascade' | 113 | onDelete: 'cascade' |
103 | }) | 114 | }) |
104 | VideoFile: VideoFileModel | 115 | VideoFile: VideoFileModel |
105 | 116 | ||
117 | @ForeignKey(() => VideoStreamingPlaylistModel) | ||
118 | @Column | ||
119 | videoStreamingPlaylistId: number | ||
120 | |||
121 | @BelongsTo(() => VideoStreamingPlaylistModel, { | ||
122 | foreignKey: { | ||
123 | allowNull: true | ||
124 | }, | ||
125 | onDelete: 'cascade' | ||
126 | }) | ||
127 | VideoStreamingPlaylist: VideoStreamingPlaylistModel | ||
128 | |||
106 | @ForeignKey(() => ActorModel) | 129 | @ForeignKey(() => ActorModel) |
107 | @Column | 130 | @Column |
108 | actorId: number | 131 | actorId: number |
@@ -119,13 +142,25 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
119 | static async removeFile (instance: VideoRedundancyModel) { | 142 | static async removeFile (instance: VideoRedundancyModel) { |
120 | if (!instance.isOwned()) return | 143 | if (!instance.isOwned()) return |
121 | 144 | ||
122 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | 145 | if (instance.videoFileId) { |
146 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | ||
123 | 147 | ||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 148 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 149 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 150 | ||
127 | videoFile.Video.removeFile(videoFile, true) | 151 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 152 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
153 | } | ||
154 | |||
155 | if (instance.videoStreamingPlaylistId) { | ||
156 | const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId) | ||
157 | |||
158 | const videoUUID = videoStreamingPlaylist.Video.uuid | ||
159 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) | ||
160 | |||
161 | videoStreamingPlaylist.Video.removeStreamingPlaylist(true) | ||
162 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) | ||
163 | } | ||
129 | 164 | ||
130 | return undefined | 165 | return undefined |
131 | } | 166 | } |
@@ -143,6 +178,19 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
143 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 178 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
144 | } | 179 | } |
145 | 180 | ||
181 | static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) { | ||
182 | const actor = await getServerActor() | ||
183 | |||
184 | const query = { | ||
185 | where: { | ||
186 | actorId: actor.id, | ||
187 | videoStreamingPlaylistId | ||
188 | } | ||
189 | } | ||
190 | |||
191 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
192 | } | ||
193 | |||
146 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 194 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
147 | const query = { | 195 | const query = { |
148 | where: { | 196 | where: { |
@@ -191,7 +239,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
191 | const ids = rows.map(r => r.id) | 239 | const ids = rows.map(r => r.id) |
192 | const id = sample(ids) | 240 | const id = sample(ids) |
193 | 241 | ||
194 | return VideoModel.loadWithFile(id, undefined, !isTestInstance()) | 242 | return VideoModel.loadWithFiles(id, undefined, !isTestInstance()) |
195 | } | 243 | } |
196 | 244 | ||
197 | static async findMostViewToDuplicate (randomizedFactor: number) { | 245 | static async findMostViewToDuplicate (randomizedFactor: number) { |
@@ -333,40 +381,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
333 | 381 | ||
334 | static async listLocalOfServer (serverId: number) { | 382 | static async listLocalOfServer (serverId: number) { |
335 | const actor = await getServerActor() | 383 | const actor = await getServerActor() |
336 | 384 | const buildVideoInclude = () => ({ | |
337 | const query = { | 385 | model: VideoModel, |
338 | where: { | 386 | required: true, |
339 | actorId: actor.id | ||
340 | }, | ||
341 | include: [ | 387 | include: [ |
342 | { | 388 | { |
343 | model: VideoFileModel, | 389 | attributes: [], |
390 | model: VideoChannelModel.unscoped(), | ||
344 | required: true, | 391 | required: true, |
345 | include: [ | 392 | include: [ |
346 | { | 393 | { |
347 | model: VideoModel, | 394 | attributes: [], |
395 | model: ActorModel.unscoped(), | ||
348 | required: true, | 396 | required: true, |
349 | include: [ | 397 | where: { |
350 | { | 398 | serverId |
351 | attributes: [], | 399 | } |
352 | model: VideoChannelModel.unscoped(), | ||
353 | required: true, | ||
354 | include: [ | ||
355 | { | ||
356 | attributes: [], | ||
357 | model: ActorModel.unscoped(), | ||
358 | required: true, | ||
359 | where: { | ||
360 | serverId | ||
361 | } | ||
362 | } | ||
363 | ] | ||
364 | } | ||
365 | ] | ||
366 | } | 400 | } |
367 | ] | 401 | ] |
368 | } | 402 | } |
369 | ] | 403 | ] |
404 | }) | ||
405 | |||
406 | const query = { | ||
407 | where: { | ||
408 | actorId: actor.id | ||
409 | }, | ||
410 | include: [ | ||
411 | { | ||
412 | model: VideoFileModel, | ||
413 | required: false, | ||
414 | include: [ buildVideoInclude() ] | ||
415 | }, | ||
416 | { | ||
417 | model: VideoStreamingPlaylistModel, | ||
418 | required: false, | ||
419 | include: [ buildVideoInclude() ] | ||
420 | } | ||
421 | ] | ||
370 | } | 422 | } |
371 | 423 | ||
372 | return VideoRedundancyModel.findAll(query) | 424 | return VideoRedundancyModel.findAll(query) |
@@ -403,11 +455,32 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
403 | })) | 455 | })) |
404 | } | 456 | } |
405 | 457 | ||
458 | getVideo () { | ||
459 | if (this.VideoFile) return this.VideoFile.Video | ||
460 | |||
461 | return this.VideoStreamingPlaylist.Video | ||
462 | } | ||
463 | |||
406 | isOwned () { | 464 | isOwned () { |
407 | return !!this.strategy | 465 | return !!this.strategy |
408 | } | 466 | } |
409 | 467 | ||
410 | toActivityPubObject (): CacheFileObject { | 468 | toActivityPubObject (): CacheFileObject { |
469 | if (this.VideoStreamingPlaylist) { | ||
470 | return { | ||
471 | id: this.url, | ||
472 | type: 'CacheFile' as 'CacheFile', | ||
473 | object: this.VideoStreamingPlaylist.Video.url, | ||
474 | expires: this.expiresOn.toISOString(), | ||
475 | url: { | ||
476 | type: 'Link', | ||
477 | mimeType: 'application/x-mpegURL', | ||
478 | mediaType: 'application/x-mpegURL', | ||
479 | href: this.fileUrl | ||
480 | } | ||
481 | } | ||
482 | } | ||
483 | |||
411 | return { | 484 | return { |
412 | id: this.url, | 485 | id: this.url, |
413 | type: 'CacheFile' as 'CacheFile', | 486 | type: 'CacheFile' as 'CacheFile', |
@@ -431,7 +504,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
431 | 504 | ||
432 | const notIn = Sequelize.literal( | 505 | const notIn = Sequelize.literal( |
433 | '(' + | 506 | '(' + |
434 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + | 507 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + |
435 | ')' | 508 | ')' |
436 | ) | 509 | ) |
437 | 510 | ||
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 1f1b76c1e..7d1e371b9 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -62,7 +62,7 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
62 | extname: string | 62 | extname: string |
63 | 63 | ||
64 | @AllowNull(false) | 64 | @AllowNull(false) |
65 | @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) | 65 | @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) |
66 | @Column | 66 | @Column |
67 | infoHash: string | 67 | infoHash: string |
68 | 68 | ||
@@ -86,14 +86,14 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
86 | 86 | ||
87 | @HasMany(() => VideoRedundancyModel, { | 87 | @HasMany(() => VideoRedundancyModel, { |
88 | foreignKey: { | 88 | foreignKey: { |
89 | allowNull: false | 89 | allowNull: true |
90 | }, | 90 | }, |
91 | onDelete: 'CASCADE', | 91 | onDelete: 'CASCADE', |
92 | hooks: true | 92 | hooks: true |
93 | }) | 93 | }) |
94 | RedundancyVideos: VideoRedundancyModel[] | 94 | RedundancyVideos: VideoRedundancyModel[] |
95 | 95 | ||
96 | static isInfohashExists (infoHash: string) { | 96 | static doesInfohashExist (infoHash: string) { |
97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
98 | const options = { | 98 | const options = { |
99 | type: Sequelize.QueryTypes.SELECT, | 99 | type: Sequelize.QueryTypes.SELECT, |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index de0747f22..e49dbee30 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -1,7 +1,12 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { VideoFileModel } from './video-file' | 3 | import { VideoFileModel } from './video-file' |
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 4 | import { |
5 | ActivityPlaylistInfohashesObject, | ||
6 | ActivityPlaylistSegmentHashesObject, | ||
7 | ActivityUrlObject, | ||
8 | VideoTorrentObject | ||
9 | } from '../../../shared/models/activitypub/objects' | ||
5 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' | 10 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' |
6 | import { VideoCaptionModel } from './video-caption' | 11 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 12 | import { |
@@ -11,6 +16,8 @@ import { | |||
11 | getVideoSharesActivityPubUrl | 16 | getVideoSharesActivityPubUrl |
12 | } from '../../lib/activitypub' | 17 | } from '../../lib/activitypub' |
13 | import { isArray } from '../../helpers/custom-validators/misc' | 18 | import { isArray } from '../../helpers/custom-validators/misc' |
19 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
20 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
14 | 21 | ||
15 | export type VideoFormattingJSONOptions = { | 22 | export type VideoFormattingJSONOptions = { |
16 | completeDescription?: boolean | 23 | completeDescription?: boolean |
@@ -120,7 +127,12 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
120 | } | 127 | } |
121 | }) | 128 | }) |
122 | 129 | ||
130 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
131 | |||
123 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | 132 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] |
133 | |||
134 | const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
135 | |||
124 | const detailsJson = { | 136 | const detailsJson = { |
125 | support: video.support, | 137 | support: video.support, |
126 | descriptionPath: video.getDescriptionAPIPath(), | 138 | descriptionPath: video.getDescriptionAPIPath(), |
@@ -133,7 +145,11 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
133 | id: video.state, | 145 | id: video.state, |
134 | label: VideoModel.getStateLabel(video.state) | 146 | label: VideoModel.getStateLabel(video.state) |
135 | }, | 147 | }, |
136 | files: [] | 148 | |
149 | trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs), | ||
150 | |||
151 | files: [], | ||
152 | streamingPlaylists | ||
137 | } | 153 | } |
138 | 154 | ||
139 | // Format and sort video files | 155 | // Format and sort video files |
@@ -142,6 +158,25 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
142 | return Object.assign(formattedJson, detailsJson) | 158 | return Object.assign(formattedJson, detailsJson) |
143 | } | 159 | } |
144 | 160 | ||
161 | function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] { | ||
162 | if (isArray(playlists) === false) return [] | ||
163 | |||
164 | return playlists | ||
165 | .map(playlist => { | ||
166 | const redundancies = isArray(playlist.RedundancyVideos) | ||
167 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
168 | : [] | ||
169 | |||
170 | return { | ||
171 | id: playlist.id, | ||
172 | type: playlist.type, | ||
173 | playlistUrl: playlist.playlistUrl, | ||
174 | segmentsSha256Url: playlist.segmentsSha256Url, | ||
175 | redundancies | ||
176 | } as VideoStreamingPlaylist | ||
177 | }) | ||
178 | } | ||
179 | |||
145 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { | 180 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { |
146 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 181 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
147 | 182 | ||
@@ -232,6 +267,28 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
232 | }) | 267 | }) |
233 | } | 268 | } |
234 | 269 | ||
270 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
271 | let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] | ||
272 | |||
273 | tag = playlist.p2pMediaLoaderInfohashes | ||
274 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) | ||
275 | tag.push({ | ||
276 | type: 'Link', | ||
277 | name: 'sha256', | ||
278 | mimeType: 'application/json' as 'application/json', | ||
279 | mediaType: 'application/json' as 'application/json', | ||
280 | href: playlist.segmentsSha256Url | ||
281 | }) | ||
282 | |||
283 | url.push({ | ||
284 | type: 'Link', | ||
285 | mimeType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
286 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
287 | href: playlist.playlistUrl, | ||
288 | tag | ||
289 | }) | ||
290 | } | ||
291 | |||
235 | // Add video url too | 292 | // Add video url too |
236 | url.push({ | 293 | url.push({ |
237 | type: 'Link', | 294 | type: 'Link', |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts new file mode 100644 index 000000000..bf6f7b0c4 --- /dev/null +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -0,0 +1,158 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
3 | import { throwIfNotValid } from '../utils' | ||
4 | import { VideoModel } from './video' | ||
5 | import * as Sequelize from 'sequelize' | ||
6 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
9 | import { CONSTRAINTS_FIELDS, STATIC_PATHS } from '../../initializers' | ||
10 | import { VideoFileModel } from './video-file' | ||
11 | import { join } from 'path' | ||
12 | import { sha1 } from '../../helpers/core-utils' | ||
13 | import { isArrayOf } from '../../helpers/custom-validators/misc' | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'videoStreamingPlaylist', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'videoId' ] | ||
20 | }, | ||
21 | { | ||
22 | fields: [ 'videoId', 'type' ], | ||
23 | unique: true | ||
24 | }, | ||
25 | { | ||
26 | fields: [ 'p2pMediaLoaderInfohashes' ], | ||
27 | using: 'gin' | ||
28 | } | ||
29 | ] | ||
30 | }) | ||
31 | export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> { | ||
32 | @CreatedAt | ||
33 | createdAt: Date | ||
34 | |||
35 | @UpdatedAt | ||
36 | updatedAt: Date | ||
37 | |||
38 | @AllowNull(false) | ||
39 | @Column | ||
40 | type: VideoStreamingPlaylistType | ||
41 | |||
42 | @AllowNull(false) | ||
43 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | ||
44 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | ||
45 | playlistUrl: string | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes')) | ||
49 | @Column(DataType.ARRAY(DataType.STRING)) | ||
50 | p2pMediaLoaderInfohashes: string[] | ||
51 | |||
52 | @AllowNull(false) | ||
53 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | ||
54 | @Column | ||
55 | segmentsSha256Url: string | ||
56 | |||
57 | @ForeignKey(() => VideoModel) | ||
58 | @Column | ||
59 | videoId: number | ||
60 | |||
61 | @BelongsTo(() => VideoModel, { | ||
62 | foreignKey: { | ||
63 | allowNull: false | ||
64 | }, | ||
65 | onDelete: 'CASCADE' | ||
66 | }) | ||
67 | Video: VideoModel | ||
68 | |||
69 | @HasMany(() => VideoRedundancyModel, { | ||
70 | foreignKey: { | ||
71 | allowNull: false | ||
72 | }, | ||
73 | onDelete: 'CASCADE', | ||
74 | hooks: true | ||
75 | }) | ||
76 | RedundancyVideos: VideoRedundancyModel[] | ||
77 | |||
78 | static doesInfohashExist (infoHash: string) { | ||
79 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | ||
80 | const options = { | ||
81 | type: Sequelize.QueryTypes.SELECT, | ||
82 | bind: { infoHash }, | ||
83 | raw: true | ||
84 | } | ||
85 | |||
86 | return VideoModel.sequelize.query(query, options) | ||
87 | .then(results => { | ||
88 | return results.length === 1 | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) { | ||
93 | const hashes: string[] = [] | ||
94 | |||
95 | // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L97 | ||
96 | for (let i = 0; i < videoFiles.length; i++) { | ||
97 | hashes.push(sha1(`1${playlistUrl}+V${i}`)) | ||
98 | } | ||
99 | |||
100 | return hashes | ||
101 | } | ||
102 | |||
103 | static loadWithVideo (id: number) { | ||
104 | const options = { | ||
105 | include: [ | ||
106 | { | ||
107 | model: VideoModel.unscoped(), | ||
108 | required: true | ||
109 | } | ||
110 | ] | ||
111 | } | ||
112 | |||
113 | return VideoStreamingPlaylistModel.findById(id, options) | ||
114 | } | ||
115 | |||
116 | static getHlsPlaylistFilename (resolution: number) { | ||
117 | return resolution + '.m3u8' | ||
118 | } | ||
119 | |||
120 | static getMasterHlsPlaylistFilename () { | ||
121 | return 'master.m3u8' | ||
122 | } | ||
123 | |||
124 | static getHlsSha256SegmentsFilename () { | ||
125 | return 'segments-sha256.json' | ||
126 | } | ||
127 | |||
128 | static getHlsVideoName (uuid: string, resolution: number) { | ||
129 | return `${uuid}-${resolution}-fragmented.mp4` | ||
130 | } | ||
131 | |||
132 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | ||
133 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
134 | } | ||
135 | |||
136 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | ||
137 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
138 | } | ||
139 | |||
140 | static getHlsSha256SegmentsStaticPath (videoUUID: string) { | ||
141 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
142 | } | ||
143 | |||
144 | getStringType () { | ||
145 | if (this.type === VideoStreamingPlaylistType.HLS) return 'hls' | ||
146 | |||
147 | return 'unknown' | ||
148 | } | ||
149 | |||
150 | getVideoRedundancyUrl (baseUrlHttp: string) { | ||
151 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid | ||
152 | } | ||
153 | |||
154 | hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) { | ||
155 | return this.type === other.type && | ||
156 | this.videoId === other.videoId | ||
157 | } | ||
158 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 80a6c7832..702260772 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -52,7 +52,7 @@ import { | |||
52 | ACTIVITY_PUB, | 52 | ACTIVITY_PUB, |
53 | API_VERSION, | 53 | API_VERSION, |
54 | CONFIG, | 54 | CONFIG, |
55 | CONSTRAINTS_FIELDS, | 55 | CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY, |
56 | PREVIEWS_SIZE, | 56 | PREVIEWS_SIZE, |
57 | REMOTE_SCHEME, | 57 | REMOTE_SCHEME, |
58 | STATIC_DOWNLOAD_PATHS, | 58 | STATIC_DOWNLOAD_PATHS, |
@@ -95,6 +95,7 @@ import * as validator from 'validator' | |||
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | 97 | import { VideoImportModel } from './video-import' |
98 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
98 | 99 | ||
99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 100 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
100 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 101 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -159,7 +160,9 @@ export enum ScopeNames { | |||
159 | WITH_FILES = 'WITH_FILES', | 160 | WITH_FILES = 'WITH_FILES', |
160 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 161 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
161 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 162 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
162 | WITH_USER_HISTORY = 'WITH_USER_HISTORY' | 163 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
164 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | ||
165 | WITH_USER_ID = 'WITH_USER_ID' | ||
163 | } | 166 | } |
164 | 167 | ||
165 | type ForAPIOptions = { | 168 | type ForAPIOptions = { |
@@ -463,6 +466,22 @@ type AvailableForListIDsOptions = { | |||
463 | 466 | ||
464 | return query | 467 | return query |
465 | }, | 468 | }, |
469 | [ ScopeNames.WITH_USER_ID ]: { | ||
470 | include: [ | ||
471 | { | ||
472 | attributes: [ 'accountId' ], | ||
473 | model: () => VideoChannelModel.unscoped(), | ||
474 | required: true, | ||
475 | include: [ | ||
476 | { | ||
477 | attributes: [ 'userId' ], | ||
478 | model: () => AccountModel.unscoped(), | ||
479 | required: true | ||
480 | } | ||
481 | ] | ||
482 | } | ||
483 | ] | ||
484 | }, | ||
466 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 485 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
467 | include: [ | 486 | include: [ |
468 | { | 487 | { |
@@ -527,22 +546,55 @@ type AvailableForListIDsOptions = { | |||
527 | } | 546 | } |
528 | ] | 547 | ] |
529 | }, | 548 | }, |
530 | [ ScopeNames.WITH_FILES ]: { | 549 | [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => { |
531 | include: [ | 550 | let subInclude: any[] = [] |
532 | { | 551 | |
533 | model: () => VideoFileModel.unscoped(), | 552 | if (withRedundancies === true) { |
534 | // FIXME: typings | 553 | subInclude = [ |
535 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | 554 | { |
536 | required: false, | 555 | attributes: [ 'fileUrl' ], |
537 | include: [ | 556 | model: VideoRedundancyModel.unscoped(), |
538 | { | 557 | required: false |
539 | attributes: [ 'fileUrl' ], | 558 | } |
540 | model: () => VideoRedundancyModel.unscoped(), | 559 | ] |
541 | required: false | 560 | } |
542 | } | 561 | |
543 | ] | 562 | return { |
544 | } | 563 | include: [ |
545 | ] | 564 | { |
565 | model: VideoFileModel.unscoped(), | ||
566 | // FIXME: typings | ||
567 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | ||
568 | required: false, | ||
569 | include: subInclude | ||
570 | } | ||
571 | ] | ||
572 | } | ||
573 | }, | ||
574 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { | ||
575 | let subInclude: any[] = [] | ||
576 | |||
577 | if (withRedundancies === true) { | ||
578 | subInclude = [ | ||
579 | { | ||
580 | attributes: [ 'fileUrl' ], | ||
581 | model: VideoRedundancyModel.unscoped(), | ||
582 | required: false | ||
583 | } | ||
584 | ] | ||
585 | } | ||
586 | |||
587 | return { | ||
588 | include: [ | ||
589 | { | ||
590 | model: VideoStreamingPlaylistModel.unscoped(), | ||
591 | // FIXME: typings | ||
592 | [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join | ||
593 | required: false, | ||
594 | include: subInclude | ||
595 | } | ||
596 | ] | ||
597 | } | ||
546 | }, | 598 | }, |
547 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { | 599 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { |
548 | include: [ | 600 | include: [ |
@@ -722,6 +774,16 @@ export class VideoModel extends Model<VideoModel> { | |||
722 | }) | 774 | }) |
723 | VideoFiles: VideoFileModel[] | 775 | VideoFiles: VideoFileModel[] |
724 | 776 | ||
777 | @HasMany(() => VideoStreamingPlaylistModel, { | ||
778 | foreignKey: { | ||
779 | name: 'videoId', | ||
780 | allowNull: false | ||
781 | }, | ||
782 | hooks: true, | ||
783 | onDelete: 'cascade' | ||
784 | }) | ||
785 | VideoStreamingPlaylists: VideoStreamingPlaylistModel[] | ||
786 | |||
725 | @HasMany(() => VideoShareModel, { | 787 | @HasMany(() => VideoShareModel, { |
726 | foreignKey: { | 788 | foreignKey: { |
727 | name: 'videoId', | 789 | name: 'videoId', |
@@ -847,6 +909,9 @@ export class VideoModel extends Model<VideoModel> { | |||
847 | tasks.push(instance.removeFile(file)) | 909 | tasks.push(instance.removeFile(file)) |
848 | tasks.push(instance.removeTorrent(file)) | 910 | tasks.push(instance.removeTorrent(file)) |
849 | }) | 911 | }) |
912 | |||
913 | // Remove playlists file | ||
914 | tasks.push(instance.removeStreamingPlaylist()) | ||
850 | } | 915 | } |
851 | 916 | ||
852 | // Do not wait video deletion because we could be in a transaction | 917 | // Do not wait video deletion because we could be in a transaction |
@@ -858,10 +923,6 @@ export class VideoModel extends Model<VideoModel> { | |||
858 | return undefined | 923 | return undefined |
859 | } | 924 | } |
860 | 925 | ||
861 | static list () { | ||
862 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll() | ||
863 | } | ||
864 | |||
865 | static listLocal () { | 926 | static listLocal () { |
866 | const query = { | 927 | const query = { |
867 | where: { | 928 | where: { |
@@ -869,7 +930,7 @@ export class VideoModel extends Model<VideoModel> { | |||
869 | } | 930 | } |
870 | } | 931 | } |
871 | 932 | ||
872 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) | 933 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) |
873 | } | 934 | } |
874 | 935 | ||
875 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 936 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
@@ -1200,6 +1261,16 @@ export class VideoModel extends Model<VideoModel> { | |||
1200 | return VideoModel.findOne(options) | 1261 | return VideoModel.findOne(options) |
1201 | } | 1262 | } |
1202 | 1263 | ||
1264 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { | ||
1265 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1266 | const options = { | ||
1267 | where, | ||
1268 | transaction: t | ||
1269 | } | ||
1270 | |||
1271 | return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) | ||
1272 | } | ||
1273 | |||
1203 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { | 1274 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
1204 | const where = VideoModel.buildWhereIdOrUUID(id) | 1275 | const where = VideoModel.buildWhereIdOrUUID(id) |
1205 | 1276 | ||
@@ -1212,8 +1283,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1212 | return VideoModel.findOne(options) | 1283 | return VideoModel.findOne(options) |
1213 | } | 1284 | } |
1214 | 1285 | ||
1215 | static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { | 1286 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1216 | return VideoModel.scope(ScopeNames.WITH_FILES) | 1287 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) |
1217 | .findById(id, { transaction: t, logging }) | 1288 | .findById(id, { transaction: t, logging }) |
1218 | } | 1289 | } |
1219 | 1290 | ||
@@ -1224,9 +1295,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1224 | } | 1295 | } |
1225 | } | 1296 | } |
1226 | 1297 | ||
1227 | return VideoModel | 1298 | return VideoModel.findOne(options) |
1228 | .scope([ ScopeNames.WITH_FILES ]) | ||
1229 | .findOne(options) | ||
1230 | } | 1299 | } |
1231 | 1300 | ||
1232 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 1301 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
@@ -1248,7 +1317,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1248 | transaction | 1317 | transaction |
1249 | } | 1318 | } |
1250 | 1319 | ||
1251 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | 1320 | return VideoModel.scope([ |
1321 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1322 | ScopeNames.WITH_FILES, | ||
1323 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1324 | ]).findOne(query) | ||
1252 | } | 1325 | } |
1253 | 1326 | ||
1254 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { | 1327 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { |
@@ -1263,9 +1336,37 @@ export class VideoModel extends Model<VideoModel> { | |||
1263 | const scopes = [ | 1336 | const scopes = [ |
1264 | ScopeNames.WITH_TAGS, | 1337 | ScopeNames.WITH_TAGS, |
1265 | ScopeNames.WITH_BLACKLISTED, | 1338 | ScopeNames.WITH_BLACKLISTED, |
1339 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1340 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1266 | ScopeNames.WITH_FILES, | 1341 | ScopeNames.WITH_FILES, |
1342 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1343 | ] | ||
1344 | |||
1345 | if (userId) { | ||
1346 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings | ||
1347 | } | ||
1348 | |||
1349 | return VideoModel | ||
1350 | .scope(scopes) | ||
1351 | .findOne(options) | ||
1352 | } | ||
1353 | |||
1354 | static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) { | ||
1355 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1356 | |||
1357 | const options = { | ||
1358 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
1359 | where, | ||
1360 | transaction: t | ||
1361 | } | ||
1362 | |||
1363 | const scopes = [ | ||
1364 | ScopeNames.WITH_TAGS, | ||
1365 | ScopeNames.WITH_BLACKLISTED, | ||
1267 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1366 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1268 | ScopeNames.WITH_SCHEDULED_UPDATE | 1367 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1368 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings | ||
1369 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings | ||
1269 | ] | 1370 | ] |
1270 | 1371 | ||
1271 | if (userId) { | 1372 | if (userId) { |
@@ -1612,6 +1713,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1612 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1713 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1613 | } | 1714 | } |
1614 | 1715 | ||
1716 | removeStreamingPlaylist (isRedundancy = false) { | ||
1717 | const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY | ||
1718 | |||
1719 | const filePath = join(baseDir, this.uuid) | ||
1720 | return remove(filePath) | ||
1721 | .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err })) | ||
1722 | } | ||
1723 | |||
1615 | isOutdated () { | 1724 | isOutdated () { |
1616 | if (this.isOwned()) return false | 1725 | if (this.isOwned()) return false |
1617 | 1726 | ||
@@ -1646,7 +1755,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1646 | 1755 | ||
1647 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { | 1756 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { |
1648 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) | 1757 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) |
1649 | const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | 1758 | const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) |
1650 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] | 1759 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] |
1651 | 1760 | ||
1652 | const redundancies = videoFile.RedundancyVideos | 1761 | const redundancies = videoFile.RedundancyVideos |
@@ -1663,6 +1772,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1663 | return magnetUtil.encode(magnetHash) | 1772 | return magnetUtil.encode(magnetHash) |
1664 | } | 1773 | } |
1665 | 1774 | ||
1775 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { | ||
1776 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | ||
1777 | } | ||
1778 | |||
1666 | getThumbnailUrl (baseUrlHttp: string) { | 1779 | getThumbnailUrl (baseUrlHttp: string) { |
1667 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | 1780 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() |
1668 | } | 1781 | } |
@@ -1686,4 +1799,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1686 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1799 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1687 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1800 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1688 | } | 1801 | } |
1802 | |||
1803 | getBandwidthBits (videoFile: VideoFileModel) { | ||
1804 | return Math.ceil((videoFile.size * 8) / this.duration) | ||
1805 | } | ||
1689 | } | 1806 | } |
diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts index 567fd072c..68f9519c6 100644 --- a/server/tests/api/check-params/accounts.ts +++ b/server/tests/api/check-params/accounts.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | } from '../../../../shared/utils/requests/check-api-params' | 10 | } from '../../../../shared/utils/requests/check-api-params' |
11 | import { getAccount } from '../../../../shared/utils/users/accounts' | 11 | import { getAccount } from '../../../../shared/utils/users/accounts' |
12 | 12 | ||
13 | describe('Test users API validators', function () { | 13 | describe('Test accounts API validators', function () { |
14 | const path = '/api/v1/accounts/' | 14 | const path = '/api/v1/accounts/' |
15 | let server: ServerInfo | 15 | let server: ServerInfo |
16 | 16 | ||
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 4038ecbf0..07de2b5a5 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -65,6 +65,9 @@ describe('Test config API validators', function () { | |||
65 | '480p': true, | 65 | '480p': true, |
66 | '720p': false, | 66 | '720p': false, |
67 | '1080p': false | 67 | '1080p': false |
68 | }, | ||
69 | hls: { | ||
70 | enabled: false | ||
68 | } | 71 | } |
69 | }, | 72 | }, |
70 | import: { | 73 | import: { |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index a3e8e2e9c..13be8b460 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -464,6 +464,24 @@ describe('Test users API validators', function () { | |||
464 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | 464 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) |
465 | }) | 465 | }) |
466 | 466 | ||
467 | it('Should fail with a too small password', async function () { | ||
468 | const fields = { | ||
469 | currentPassword: 'my super password', | ||
470 | password: 'bla' | ||
471 | } | ||
472 | |||
473 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
474 | }) | ||
475 | |||
476 | it('Should fail with a too long password', async function () { | ||
477 | const fields = { | ||
478 | currentPassword: 'my super password', | ||
479 | password: 'super'.repeat(61) | ||
480 | } | ||
481 | |||
482 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
483 | }) | ||
484 | |||
467 | it('Should fail with an non authenticated user', async function () { | 485 | it('Should fail with an non authenticated user', async function () { |
468 | const fields = { | 486 | const fields = { |
469 | videoQuota: 42 | 487 | videoQuota: 42 |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 9d3ce8153..778611fff 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | viewVideo, | 17 | viewVideo, |
18 | wait, | 18 | wait, |
19 | waitUntilLog, | 19 | waitUntilLog, |
20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken | 20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash |
21 | } from '../../../../shared/utils' | 21 | } from '../../../../shared/utils' |
22 | import { waitJobs } from '../../../../shared/utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | 23 | ||
@@ -48,6 +48,11 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe | |||
48 | 48 | ||
49 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | 49 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { |
50 | const config = { | 50 | const config = { |
51 | transcoding: { | ||
52 | hls: { | ||
53 | enabled: true | ||
54 | } | ||
55 | }, | ||
51 | redundancy: { | 56 | redundancy: { |
52 | videos: { | 57 | videos: { |
53 | check_interval: '5 seconds', | 58 | check_interval: '5 seconds', |
@@ -85,7 +90,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams: | |||
85 | await waitJobs(servers) | 90 | await waitJobs(servers) |
86 | } | 91 | } |
87 | 92 | ||
88 | async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { | 93 | async function check1WebSeed (videoUUID?: string) { |
89 | if (!videoUUID) videoUUID = video1Server2UUID | 94 | if (!videoUUID) videoUUID = video1Server2UUID |
90 | 95 | ||
91 | const webseeds = [ | 96 | const webseeds = [ |
@@ -93,47 +98,17 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str | |||
93 | ] | 98 | ] |
94 | 99 | ||
95 | for (const server of servers) { | 100 | for (const server of servers) { |
96 | { | 101 | // With token to avoid issues with video follow constraints |
97 | // With token to avoid issues with video follow constraints | 102 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) |
98 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) | ||
99 | 103 | ||
100 | const video: VideoDetails = res.body | 104 | const video: VideoDetails = res.body |
101 | for (const f of video.files) { | 105 | for (const f of video.files) { |
102 | checkMagnetWebseeds(f, webseeds, server) | 106 | checkMagnetWebseeds(f, webseeds, server) |
103 | } | ||
104 | } | 107 | } |
105 | } | 108 | } |
106 | } | 109 | } |
107 | 110 | ||
108 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | 111 | async function check2Webseeds (videoUUID?: string) { |
109 | const res = await getStats(servers[0].url) | ||
110 | const data: ServerStats = res.body | ||
111 | |||
112 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
113 | const stat = data.videosRedundancy[0] | ||
114 | |||
115 | expect(stat.strategy).to.equal(strategy) | ||
116 | expect(stat.totalSize).to.equal(204800) | ||
117 | expect(stat.totalUsed).to.be.at.least(1).and.below(204801) | ||
118 | expect(stat.totalVideoFiles).to.equal(4) | ||
119 | expect(stat.totalVideos).to.equal(1) | ||
120 | } | ||
121 | |||
122 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
123 | const res = await getStats(servers[0].url) | ||
124 | const data: ServerStats = res.body | ||
125 | |||
126 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
127 | |||
128 | const stat = data.videosRedundancy[0] | ||
129 | expect(stat.strategy).to.equal(strategy) | ||
130 | expect(stat.totalSize).to.equal(204800) | ||
131 | expect(stat.totalUsed).to.equal(0) | ||
132 | expect(stat.totalVideoFiles).to.equal(0) | ||
133 | expect(stat.totalVideos).to.equal(0) | ||
134 | } | ||
135 | |||
136 | async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
137 | if (!videoUUID) videoUUID = video1Server2UUID | 112 | if (!videoUUID) videoUUID = video1Server2UUID |
138 | 113 | ||
139 | const webseeds = [ | 114 | const webseeds = [ |
@@ -158,7 +133,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
158 | await makeGetRequest({ | 133 | await makeGetRequest({ |
159 | url: servers[1].url, | 134 | url: servers[1].url, |
160 | statusCodeExpected: 200, | 135 | statusCodeExpected: 200, |
161 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | 136 | path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`, |
162 | contentType: null | 137 | contentType: null |
163 | }) | 138 | }) |
164 | } | 139 | } |
@@ -174,6 +149,85 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
174 | } | 149 | } |
175 | } | 150 | } |
176 | 151 | ||
152 | async function check0PlaylistRedundancies (videoUUID?: string) { | ||
153 | if (!videoUUID) videoUUID = video1Server2UUID | ||
154 | |||
155 | for (const server of servers) { | ||
156 | // With token to avoid issues with video follow constraints | ||
157 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) | ||
158 | const video: VideoDetails = res.body | ||
159 | |||
160 | expect(video.streamingPlaylists).to.be.an('array') | ||
161 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
162 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | async function check1PlaylistRedundancies (videoUUID?: string) { | ||
167 | if (!videoUUID) videoUUID = video1Server2UUID | ||
168 | |||
169 | for (const server of servers) { | ||
170 | const res = await getVideo(server.url, videoUUID) | ||
171 | const video: VideoDetails = res.body | ||
172 | |||
173 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
174 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1) | ||
175 | |||
176 | const redundancy = video.streamingPlaylists[0].redundancies[0] | ||
177 | |||
178 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) | ||
179 | } | ||
180 | |||
181 | const baseUrlPlaylist = servers[1].url + '/static/playlists/hls' | ||
182 | const baseUrlSegment = servers[0].url + '/static/redundancy/hls' | ||
183 | |||
184 | const res = await getVideo(servers[0].url, videoUUID) | ||
185 | const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] | ||
186 | |||
187 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
188 | await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) | ||
189 | } | ||
190 | |||
191 | for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) { | ||
192 | const files = await readdir(join(root(), directory, videoUUID)) | ||
193 | expect(files).to.have.length.at.least(4) | ||
194 | |||
195 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
196 | const filename = `${videoUUID}-${resolution}-fragmented.mp4` | ||
197 | |||
198 | expect(files.find(f => f === filename)).to.not.be.undefined | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | |||
203 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | ||
204 | const res = await getStats(servers[0].url) | ||
205 | const data: ServerStats = res.body | ||
206 | |||
207 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
208 | const stat = data.videosRedundancy[0] | ||
209 | |||
210 | expect(stat.strategy).to.equal(strategy) | ||
211 | expect(stat.totalSize).to.equal(204800) | ||
212 | expect(stat.totalUsed).to.be.at.least(1).and.below(204801) | ||
213 | expect(stat.totalVideoFiles).to.equal(4) | ||
214 | expect(stat.totalVideos).to.equal(1) | ||
215 | } | ||
216 | |||
217 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
218 | const res = await getStats(servers[0].url) | ||
219 | const data: ServerStats = res.body | ||
220 | |||
221 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
222 | |||
223 | const stat = data.videosRedundancy[0] | ||
224 | expect(stat.strategy).to.equal(strategy) | ||
225 | expect(stat.totalSize).to.equal(204800) | ||
226 | expect(stat.totalUsed).to.equal(0) | ||
227 | expect(stat.totalVideoFiles).to.equal(0) | ||
228 | expect(stat.totalVideos).to.equal(0) | ||
229 | } | ||
230 | |||
177 | async function enableRedundancyOnServer1 () { | 231 | async function enableRedundancyOnServer1 () { |
178 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | 232 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) |
179 | 233 | ||
@@ -220,7 +274,8 @@ describe('Test videos redundancy', function () { | |||
220 | }) | 274 | }) |
221 | 275 | ||
222 | it('Should have 1 webseed on the first video', async function () { | 276 | it('Should have 1 webseed on the first video', async function () { |
223 | await check1WebSeed(strategy) | 277 | await check1WebSeed() |
278 | await check0PlaylistRedundancies() | ||
224 | await checkStatsWith1Webseed(strategy) | 279 | await checkStatsWith1Webseed(strategy) |
225 | }) | 280 | }) |
226 | 281 | ||
@@ -229,27 +284,29 @@ describe('Test videos redundancy', function () { | |||
229 | }) | 284 | }) |
230 | 285 | ||
231 | it('Should have 2 webseeds on the first video', async function () { | 286 | it('Should have 2 webseeds on the first video', async function () { |
232 | this.timeout(40000) | 287 | this.timeout(80000) |
233 | 288 | ||
234 | await waitJobs(servers) | 289 | await waitJobs(servers) |
235 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 290 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
236 | await waitJobs(servers) | 291 | await waitJobs(servers) |
237 | 292 | ||
238 | await check2Webseeds(strategy) | 293 | await check2Webseeds() |
294 | await check1PlaylistRedundancies() | ||
239 | await checkStatsWith2Webseed(strategy) | 295 | await checkStatsWith2Webseed(strategy) |
240 | }) | 296 | }) |
241 | 297 | ||
242 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | 298 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { |
243 | this.timeout(40000) | 299 | this.timeout(80000) |
244 | 300 | ||
245 | await disableRedundancyOnServer1() | 301 | await disableRedundancyOnServer1() |
246 | 302 | ||
247 | await waitJobs(servers) | 303 | await waitJobs(servers) |
248 | await wait(5000) | 304 | await wait(5000) |
249 | 305 | ||
250 | await check1WebSeed(strategy) | 306 | await check1WebSeed() |
307 | await check0PlaylistRedundancies() | ||
251 | 308 | ||
252 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 309 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos', join('playlists', 'hls') ]) |
253 | }) | 310 | }) |
254 | 311 | ||
255 | after(function () { | 312 | after(function () { |
@@ -267,7 +324,8 @@ describe('Test videos redundancy', function () { | |||
267 | }) | 324 | }) |
268 | 325 | ||
269 | it('Should have 1 webseed on the first video', async function () { | 326 | it('Should have 1 webseed on the first video', async function () { |
270 | await check1WebSeed(strategy) | 327 | await check1WebSeed() |
328 | await check0PlaylistRedundancies() | ||
271 | await checkStatsWith1Webseed(strategy) | 329 | await checkStatsWith1Webseed(strategy) |
272 | }) | 330 | }) |
273 | 331 | ||
@@ -276,25 +334,27 @@ describe('Test videos redundancy', function () { | |||
276 | }) | 334 | }) |
277 | 335 | ||
278 | it('Should have 2 webseeds on the first video', async function () { | 336 | it('Should have 2 webseeds on the first video', async function () { |
279 | this.timeout(40000) | 337 | this.timeout(80000) |
280 | 338 | ||
281 | await waitJobs(servers) | 339 | await waitJobs(servers) |
282 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 340 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
283 | await waitJobs(servers) | 341 | await waitJobs(servers) |
284 | 342 | ||
285 | await check2Webseeds(strategy) | 343 | await check2Webseeds() |
344 | await check1PlaylistRedundancies() | ||
286 | await checkStatsWith2Webseed(strategy) | 345 | await checkStatsWith2Webseed(strategy) |
287 | }) | 346 | }) |
288 | 347 | ||
289 | it('Should unfollow on server 1 and remove duplicated videos', async function () { | 348 | it('Should unfollow on server 1 and remove duplicated videos', async function () { |
290 | this.timeout(40000) | 349 | this.timeout(80000) |
291 | 350 | ||
292 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | 351 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) |
293 | 352 | ||
294 | await waitJobs(servers) | 353 | await waitJobs(servers) |
295 | await wait(5000) | 354 | await wait(5000) |
296 | 355 | ||
297 | await check1WebSeed(strategy) | 356 | await check1WebSeed() |
357 | await check0PlaylistRedundancies() | ||
298 | 358 | ||
299 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 359 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) |
300 | }) | 360 | }) |
@@ -314,7 +374,8 @@ describe('Test videos redundancy', function () { | |||
314 | }) | 374 | }) |
315 | 375 | ||
316 | it('Should have 1 webseed on the first video', async function () { | 376 | it('Should have 1 webseed on the first video', async function () { |
317 | await check1WebSeed(strategy) | 377 | await check1WebSeed() |
378 | await check0PlaylistRedundancies() | ||
318 | await checkStatsWith1Webseed(strategy) | 379 | await checkStatsWith1Webseed(strategy) |
319 | }) | 380 | }) |
320 | 381 | ||
@@ -323,18 +384,19 @@ describe('Test videos redundancy', function () { | |||
323 | }) | 384 | }) |
324 | 385 | ||
325 | it('Should still have 1 webseed on the first video', async function () { | 386 | it('Should still have 1 webseed on the first video', async function () { |
326 | this.timeout(40000) | 387 | this.timeout(80000) |
327 | 388 | ||
328 | await waitJobs(servers) | 389 | await waitJobs(servers) |
329 | await wait(15000) | 390 | await wait(15000) |
330 | await waitJobs(servers) | 391 | await waitJobs(servers) |
331 | 392 | ||
332 | await check1WebSeed(strategy) | 393 | await check1WebSeed() |
394 | await check0PlaylistRedundancies() | ||
333 | await checkStatsWith1Webseed(strategy) | 395 | await checkStatsWith1Webseed(strategy) |
334 | }) | 396 | }) |
335 | 397 | ||
336 | it('Should view 2 times the first video to have > min_views config', async function () { | 398 | it('Should view 2 times the first video to have > min_views config', async function () { |
337 | this.timeout(40000) | 399 | this.timeout(80000) |
338 | 400 | ||
339 | await viewVideo(servers[ 0 ].url, video1Server2UUID) | 401 | await viewVideo(servers[ 0 ].url, video1Server2UUID) |
340 | await viewVideo(servers[ 2 ].url, video1Server2UUID) | 402 | await viewVideo(servers[ 2 ].url, video1Server2UUID) |
@@ -344,13 +406,14 @@ describe('Test videos redundancy', function () { | |||
344 | }) | 406 | }) |
345 | 407 | ||
346 | it('Should have 2 webseeds on the first video', async function () { | 408 | it('Should have 2 webseeds on the first video', async function () { |
347 | this.timeout(40000) | 409 | this.timeout(80000) |
348 | 410 | ||
349 | await waitJobs(servers) | 411 | await waitJobs(servers) |
350 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 412 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
351 | await waitJobs(servers) | 413 | await waitJobs(servers) |
352 | 414 | ||
353 | await check2Webseeds(strategy) | 415 | await check2Webseeds() |
416 | await check1PlaylistRedundancies() | ||
354 | await checkStatsWith2Webseed(strategy) | 417 | await checkStatsWith2Webseed(strategy) |
355 | }) | 418 | }) |
356 | 419 | ||
@@ -405,7 +468,7 @@ describe('Test videos redundancy', function () { | |||
405 | }) | 468 | }) |
406 | 469 | ||
407 | it('Should still have 2 webseeds after 10 seconds', async function () { | 470 | it('Should still have 2 webseeds after 10 seconds', async function () { |
408 | this.timeout(40000) | 471 | this.timeout(80000) |
409 | 472 | ||
410 | await wait(10000) | 473 | await wait(10000) |
411 | 474 | ||
@@ -420,7 +483,7 @@ describe('Test videos redundancy', function () { | |||
420 | }) | 483 | }) |
421 | 484 | ||
422 | it('Should stop server 1 and expire video redundancy', async function () { | 485 | it('Should stop server 1 and expire video redundancy', async function () { |
423 | this.timeout(40000) | 486 | this.timeout(80000) |
424 | 487 | ||
425 | killallServers([ servers[0] ]) | 488 | killallServers([ servers[0] ]) |
426 | 489 | ||
@@ -446,10 +509,11 @@ describe('Test videos redundancy', function () { | |||
446 | await enableRedundancyOnServer1() | 509 | await enableRedundancyOnServer1() |
447 | 510 | ||
448 | await waitJobs(servers) | 511 | await waitJobs(servers) |
449 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 512 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
450 | await waitJobs(servers) | 513 | await waitJobs(servers) |
451 | 514 | ||
452 | await check2Webseeds(strategy) | 515 | await check2Webseeds() |
516 | await check1PlaylistRedundancies() | ||
453 | await checkStatsWith2Webseed(strategy) | 517 | await checkStatsWith2Webseed(strategy) |
454 | 518 | ||
455 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | 519 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) |
@@ -467,8 +531,10 @@ describe('Test videos redundancy', function () { | |||
467 | await wait(1000) | 531 | await wait(1000) |
468 | 532 | ||
469 | try { | 533 | try { |
470 | await check1WebSeed(strategy, video1Server2UUID) | 534 | await check1WebSeed(video1Server2UUID) |
471 | await check2Webseeds(strategy, video2Server2UUID) | 535 | await check0PlaylistRedundancies(video1Server2UUID) |
536 | await check2Webseeds(video2Server2UUID) | ||
537 | await check1PlaylistRedundancies(video2Server2UUID) | ||
472 | 538 | ||
473 | checked = true | 539 | checked = true |
474 | } catch { | 540 | } catch { |
@@ -477,6 +543,26 @@ describe('Test videos redundancy', function () { | |||
477 | } | 543 | } |
478 | }) | 544 | }) |
479 | 545 | ||
546 | it('Should disable strategy and remove redundancies', async function () { | ||
547 | this.timeout(80000) | ||
548 | |||
549 | await waitJobs(servers) | ||
550 | |||
551 | killallServers([ servers[ 0 ] ]) | ||
552 | await reRunServer(servers[ 0 ], { | ||
553 | redundancy: { | ||
554 | videos: { | ||
555 | check_interval: '1 second', | ||
556 | strategies: [] | ||
557 | } | ||
558 | } | ||
559 | }) | ||
560 | |||
561 | await waitJobs(servers) | ||
562 | |||
563 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ join('redundancy', 'hls') ]) | ||
564 | }) | ||
565 | |||
480 | after(function () { | 566 | after(function () { |
481 | return cleanServers() | 567 | return cleanServers() |
482 | }) | 568 | }) |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index bebfc7398..0dfe6e4fe 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -57,6 +57,8 @@ function checkInitialConfig (data: CustomConfig) { | |||
57 | expect(data.transcoding.resolutions['480p']).to.be.true | 57 | expect(data.transcoding.resolutions['480p']).to.be.true |
58 | expect(data.transcoding.resolutions['720p']).to.be.true | 58 | expect(data.transcoding.resolutions['720p']).to.be.true |
59 | expect(data.transcoding.resolutions['1080p']).to.be.true | 59 | expect(data.transcoding.resolutions['1080p']).to.be.true |
60 | expect(data.transcoding.hls.enabled).to.be.true | ||
61 | |||
60 | expect(data.import.videos.http.enabled).to.be.true | 62 | expect(data.import.videos.http.enabled).to.be.true |
61 | expect(data.import.videos.torrent.enabled).to.be.true | 63 | expect(data.import.videos.torrent.enabled).to.be.true |
62 | } | 64 | } |
@@ -95,6 +97,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
95 | expect(data.transcoding.resolutions['480p']).to.be.true | 97 | expect(data.transcoding.resolutions['480p']).to.be.true |
96 | expect(data.transcoding.resolutions['720p']).to.be.false | 98 | expect(data.transcoding.resolutions['720p']).to.be.false |
97 | expect(data.transcoding.resolutions['1080p']).to.be.false | 99 | expect(data.transcoding.resolutions['1080p']).to.be.false |
100 | expect(data.transcoding.hls.enabled).to.be.false | ||
98 | 101 | ||
99 | expect(data.import.videos.http.enabled).to.be.false | 102 | expect(data.import.videos.http.enabled).to.be.false |
100 | expect(data.import.videos.torrent.enabled).to.be.false | 103 | expect(data.import.videos.torrent.enabled).to.be.false |
@@ -205,6 +208,9 @@ describe('Test config', function () { | |||
205 | '480p': true, | 208 | '480p': true, |
206 | '720p': false, | 209 | '720p': false, |
207 | '1080p': false | 210 | '1080p': false |
211 | }, | ||
212 | hls: { | ||
213 | enabled: false | ||
208 | } | 214 | } |
209 | }, | 215 | }, |
210 | import: { | 216 | import: { |
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts index 5260d64cc..69e51677e 100644 --- a/server/tests/api/users/user-notifications.ts +++ b/server/tests/api/users/user-notifications.ts | |||
@@ -165,6 +165,8 @@ describe('Test users notifications', function () { | |||
165 | }) | 165 | }) |
166 | 166 | ||
167 | it('Should not send notifications if the user does not follow the video publisher', async function () { | 167 | it('Should not send notifications if the user does not follow the video publisher', async function () { |
168 | this.timeout(10000) | ||
169 | |||
168 | await uploadVideoByLocalAccount(servers) | 170 | await uploadVideoByLocalAccount(servers) |
169 | 171 | ||
170 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | 172 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) |
@@ -644,6 +646,8 @@ describe('Test users notifications', function () { | |||
644 | }) | 646 | }) |
645 | 647 | ||
646 | it('Should not send a notification if transcoding is not enabled', async function () { | 648 | it('Should not send a notification if transcoding is not enabled', async function () { |
649 | this.timeout(10000) | ||
650 | |||
647 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | 651 | const { name, uuid } = await uploadVideoByLocalAccount(servers) |
648 | await waitJobs(servers) | 652 | await waitJobs(servers) |
649 | 653 | ||
@@ -717,6 +721,24 @@ describe('Test users notifications', function () { | |||
717 | await wait(6000) | 721 | await wait(6000) |
718 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | 722 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') |
719 | }) | 723 | }) |
724 | |||
725 | it('Should not send a notification before the video is published', async function () { | ||
726 | this.timeout(20000) | ||
727 | |||
728 | let updateAt = new Date(new Date().getTime() + 100000) | ||
729 | |||
730 | const data = { | ||
731 | privacy: VideoPrivacy.PRIVATE, | ||
732 | scheduleUpdate: { | ||
733 | updateAt: updateAt.toISOString(), | ||
734 | privacy: VideoPrivacy.PUBLIC | ||
735 | } | ||
736 | } | ||
737 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
738 | |||
739 | await wait(6000) | ||
740 | await checkVideoIsPublished(baseParams, name, uuid, 'absence') | ||
741 | }) | ||
720 | }) | 742 | }) |
721 | 743 | ||
722 | describe('My video is imported', function () { | 744 | describe('My video is imported', function () { |
@@ -781,6 +803,8 @@ describe('Test users notifications', function () { | |||
781 | }) | 803 | }) |
782 | 804 | ||
783 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | 805 | it('Should send a notification only to moderators when a user registers on the instance', async function () { |
806 | this.timeout(10000) | ||
807 | |||
784 | await registerUser(servers[0].url, 'user_45', 'password') | 808 | await registerUser(servers[0].url, 'user_45', 'password') |
785 | 809 | ||
786 | await waitJobs(servers) | 810 | await waitJobs(servers) |
@@ -849,6 +873,8 @@ describe('Test users notifications', function () { | |||
849 | }) | 873 | }) |
850 | 874 | ||
851 | it('Should notify when a local account is following one of our channel', async function () { | 875 | it('Should notify when a local account is following one of our channel', async function () { |
876 | this.timeout(10000) | ||
877 | |||
852 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') | 878 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') |
853 | 879 | ||
854 | await waitJobs(servers) | 880 | await waitJobs(servers) |
@@ -857,6 +883,8 @@ describe('Test users notifications', function () { | |||
857 | }) | 883 | }) |
858 | 884 | ||
859 | it('Should notify when a remote account is following one of our channel', async function () { | 885 | it('Should notify when a remote account is following one of our channel', async function () { |
886 | this.timeout(10000) | ||
887 | |||
860 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') | 888 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') |
861 | 889 | ||
862 | await waitJobs(servers) | 890 | await waitJobs(servers) |
@@ -926,6 +954,8 @@ describe('Test users notifications', function () { | |||
926 | }) | 954 | }) |
927 | 955 | ||
928 | it('Should not have notifications', async function () { | 956 | it('Should not have notifications', async function () { |
957 | this.timeout(10000) | ||
958 | |||
929 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 959 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
930 | newVideoFromSubscription: UserNotificationSettingValue.NONE | 960 | newVideoFromSubscription: UserNotificationSettingValue.NONE |
931 | })) | 961 | })) |
@@ -943,6 +973,8 @@ describe('Test users notifications', function () { | |||
943 | }) | 973 | }) |
944 | 974 | ||
945 | it('Should only have web notifications', async function () { | 975 | it('Should only have web notifications', async function () { |
976 | this.timeout(10000) | ||
977 | |||
946 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 978 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
947 | newVideoFromSubscription: UserNotificationSettingValue.WEB | 979 | newVideoFromSubscription: UserNotificationSettingValue.WEB |
948 | })) | 980 | })) |
@@ -967,6 +999,8 @@ describe('Test users notifications', function () { | |||
967 | }) | 999 | }) |
968 | 1000 | ||
969 | it('Should only have mail notifications', async function () { | 1001 | it('Should only have mail notifications', async function () { |
1002 | this.timeout(10000) | ||
1003 | |||
970 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 1004 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
971 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL | 1005 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL |
972 | })) | 1006 | })) |
@@ -991,6 +1025,8 @@ describe('Test users notifications', function () { | |||
991 | }) | 1025 | }) |
992 | 1026 | ||
993 | it('Should have email and web notifications', async function () { | 1027 | it('Should have email and web notifications', async function () { |
1028 | this.timeout(10000) | ||
1029 | |||
994 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 1030 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
995 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | 1031 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL |
996 | })) | 1032 | })) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index ad98ab1c7..c4465d541 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -501,6 +501,22 @@ describe('Test users', function () { | |||
501 | accessTokenUser = await userLogin(server, user) | 501 | accessTokenUser = await userLogin(server, user) |
502 | }) | 502 | }) |
503 | 503 | ||
504 | it('Should be able to update another user password', async function () { | ||
505 | await updateUser({ | ||
506 | url: server.url, | ||
507 | userId, | ||
508 | accessToken, | ||
509 | password: 'password updated' | ||
510 | }) | ||
511 | |||
512 | await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401) | ||
513 | |||
514 | await userLogin(server, user, 400) | ||
515 | |||
516 | user.password = 'password updated' | ||
517 | accessTokenUser = await userLogin(server, user) | ||
518 | }) | ||
519 | |||
504 | it('Should be able to list video blacklist by a moderator', async function () { | 520 | it('Should be able to list video blacklist by a moderator', async function () { |
505 | await getBlacklistedVideosList(server.url, accessTokenUser) | 521 | await getBlacklistedVideosList(server.url, accessTokenUser) |
506 | }) | 522 | }) |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 97f467aae..a501a80b2 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -8,6 +8,7 @@ import './video-change-ownership' | |||
8 | import './video-channels' | 8 | import './video-channels' |
9 | import './video-comments' | 9 | import './video-comments' |
10 | import './video-description' | 10 | import './video-description' |
11 | import './video-hls' | ||
11 | import './video-imports' | 12 | import './video-imports' |
12 | import './video-nsfw' | 13 | import './video-nsfw' |
13 | import './video-privacy' | 14 | import './video-privacy' |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts new file mode 100644 index 000000000..a1214bad1 --- /dev/null +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -0,0 +1,139 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | checkDirectoryIsEmpty, | ||
7 | checkSegmentHash, | ||
8 | checkTmpIsEmpty, | ||
9 | doubleFollow, | ||
10 | flushAndRunMultipleServers, | ||
11 | flushTests, | ||
12 | getPlaylist, | ||
13 | getVideo, | ||
14 | killallServers, | ||
15 | removeVideo, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | updateVideo, | ||
19 | uploadVideo, | ||
20 | waitJobs | ||
21 | } from '../../../../shared/utils' | ||
22 | import { VideoDetails } from '../../../../shared/models/videos' | ||
23 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
24 | import { join } from 'path' | ||
25 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | ||
29 | const resolutions = [ 240, 360, 480, 720 ] | ||
30 | |||
31 | for (const server of servers) { | ||
32 | const res = await getVideo(server.url, videoUUID) | ||
33 | const videoDetails: VideoDetails = res.body | ||
34 | |||
35 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
36 | |||
37 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
38 | expect(hlsPlaylist).to.not.be.undefined | ||
39 | |||
40 | { | ||
41 | const res2 = await getPlaylist(hlsPlaylist.playlistUrl) | ||
42 | |||
43 | const masterPlaylist = res2.text | ||
44 | |||
45 | expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25') | ||
46 | |||
47 | for (const resolution of resolutions) { | ||
48 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | { | ||
53 | for (const resolution of resolutions) { | ||
54 | const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`) | ||
55 | |||
56 | const subPlaylist = res2.text | ||
57 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) | ||
58 | } | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const baseUrl = 'http://localhost:9001/static/playlists/hls' | ||
63 | |||
64 | for (const resolution of resolutions) { | ||
65 | await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | describe('Test HLS videos', function () { | ||
72 | let servers: ServerInfo[] = [] | ||
73 | let videoUUID = '' | ||
74 | |||
75 | before(async function () { | ||
76 | this.timeout(120000) | ||
77 | |||
78 | servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } }) | ||
79 | |||
80 | // Get the access tokens | ||
81 | await setAccessTokensToServers(servers) | ||
82 | |||
83 | // Server 1 and server 2 follow each other | ||
84 | await doubleFollow(servers[0], servers[1]) | ||
85 | }) | ||
86 | |||
87 | it('Should upload a video and transcode it to HLS', async function () { | ||
88 | this.timeout(120000) | ||
89 | |||
90 | { | ||
91 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) | ||
92 | videoUUID = res.body.video.uuid | ||
93 | } | ||
94 | |||
95 | await waitJobs(servers) | ||
96 | |||
97 | await checkHlsPlaylist(servers, videoUUID) | ||
98 | }) | ||
99 | |||
100 | it('Should update the video', async function () { | ||
101 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) | ||
102 | |||
103 | await waitJobs(servers) | ||
104 | |||
105 | await checkHlsPlaylist(servers, videoUUID) | ||
106 | }) | ||
107 | |||
108 | it('Should delete the video', async function () { | ||
109 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) | ||
110 | |||
111 | await waitJobs(servers) | ||
112 | |||
113 | for (const server of servers) { | ||
114 | await getVideo(server.url, videoUUID, 404) | ||
115 | } | ||
116 | }) | ||
117 | |||
118 | it('Should have the playlists/segment deleted from the disk', async function () { | ||
119 | for (const server of servers) { | ||
120 | await checkDirectoryIsEmpty(server, 'videos') | ||
121 | await checkDirectoryIsEmpty(server, join('playlists', 'hls')) | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | it('Should have an empty tmp directory', async function () { | ||
126 | for (const server of servers) { | ||
127 | await checkTmpIsEmpty(server) | ||
128 | } | ||
129 | }) | ||
130 | |||
131 | after(async function () { | ||
132 | killallServers(servers) | ||
133 | |||
134 | // Keep the logs if the test failed | ||
135 | if (this['ok']) { | ||
136 | await flushTests() | ||
137 | } | ||
138 | }) | ||
139 | }) | ||
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts index 811ea6a9f..d38bb4331 100644 --- a/server/tests/cli/update-host.ts +++ b/server/tests/cli/update-host.ts | |||
@@ -86,6 +86,13 @@ describe('Test update host scripts', function () { | |||
86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) | 86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) |
87 | 87 | ||
88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) | 88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) |
89 | |||
90 | const res = await getVideo(server.url, video.uuid) | ||
91 | const videoDetails: VideoDetails = res.body | ||
92 | |||
93 | expect(videoDetails.trackerUrls[0]).to.include(server.host) | ||
94 | expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host) | ||
95 | expect(videoDetails.streamingPlaylists[0].segmentsSha256Url).to.include(server.host) | ||
89 | } | 96 | } |
90 | }) | 97 | }) |
91 | 98 | ||
@@ -100,7 +107,7 @@ describe('Test update host scripts', function () { | |||
100 | } | 107 | } |
101 | }) | 108 | }) |
102 | 109 | ||
103 | it('Should have update accounts url', async function () { | 110 | it('Should have updated accounts url', async function () { |
104 | const res = await getAccountsList(server.url) | 111 | const res = await getAccountsList(server.url) |
105 | expect(res.body.total).to.equal(3) | 112 | expect(res.body.total).to.equal(3) |
106 | 113 | ||
@@ -112,7 +119,7 @@ describe('Test update host scripts', function () { | |||
112 | } | 119 | } |
113 | }) | 120 | }) |
114 | 121 | ||
115 | it('Should update torrent hosts', async function () { | 122 | it('Should have updated torrent hosts', async function () { |
116 | this.timeout(30000) | 123 | this.timeout(30000) |
117 | 124 | ||
118 | const res = await getVideosList(server.url) | 125 | const res = await getVideosList(server.url) |
diff --git a/shared/models/activitypub/activitypub-ordered-collection.ts b/shared/models/activitypub/activitypub-ordered-collection.ts index dfec0bb76..3de0890bb 100644 --- a/shared/models/activitypub/activitypub-ordered-collection.ts +++ b/shared/models/activitypub/activitypub-ordered-collection.ts | |||
@@ -2,6 +2,9 @@ export interface ActivityPubOrderedCollection<T> { | |||
2 | '@context': string[] | 2 | '@context': string[] |
3 | type: 'OrderedCollection' | 'OrderedCollectionPage' | 3 | type: 'OrderedCollection' | 'OrderedCollectionPage' |
4 | totalItems: number | 4 | totalItems: number |
5 | partOf?: string | ||
6 | orderedItems: T[] | 5 | orderedItems: T[] |
6 | |||
7 | partOf?: string | ||
8 | next?: string | ||
9 | first?: string | ||
7 | } | 10 | } |
diff --git a/shared/models/activitypub/objects/cache-file-object.ts b/shared/models/activitypub/objects/cache-file-object.ts index 0a5125f5b..4b0a3a724 100644 --- a/shared/models/activitypub/objects/cache-file-object.ts +++ b/shared/models/activitypub/objects/cache-file-object.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { ActivityVideoUrlObject } from './common-objects' | 1 | import { ActivityVideoUrlObject, ActivityPlaylistUrlObject } from './common-objects' |
2 | 2 | ||
3 | export interface CacheFileObject { | 3 | export interface CacheFileObject { |
4 | id: string | 4 | id: string |
5 | type: 'CacheFile', | 5 | type: 'CacheFile', |
6 | object: string | 6 | object: string |
7 | expires: string | 7 | expires: string |
8 | url: ActivityVideoUrlObject | 8 | url: ActivityVideoUrlObject | ActivityPlaylistUrlObject |
9 | } | 9 | } |
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 118a4f43d..8c89810d6 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts | |||
@@ -28,25 +28,47 @@ export type ActivityVideoUrlObject = { | |||
28 | fps: number | 28 | fps: number |
29 | } | 29 | } |
30 | 30 | ||
31 | export type ActivityUrlObject = | 31 | export type ActivityPlaylistSegmentHashesObject = { |
32 | ActivityVideoUrlObject | 32 | type: 'Link' |
33 | | | 33 | name: 'sha256' |
34 | { | 34 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
35 | type: 'Link' | 35 | mimeType?: 'application/json' |
36 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) | 36 | mediaType: 'application/json' |
37 | mimeType?: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet' | 37 | href: string |
38 | mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet' | 38 | } |
39 | href: string | 39 | |
40 | height: number | 40 | export type ActivityPlaylistInfohashesObject = { |
41 | } | 41 | type: 'Infohash' |
42 | | | 42 | name: string |
43 | { | 43 | } |
44 | type: 'Link' | 44 | |
45 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) | 45 | export type ActivityPlaylistUrlObject = { |
46 | mimeType?: 'text/html' | 46 | type: 'Link' |
47 | mediaType: 'text/html' | 47 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
48 | href: string | 48 | mimeType?: 'application/x-mpegURL' |
49 | } | 49 | mediaType: 'application/x-mpegURL' |
50 | href: string | ||
51 | tag?: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] | ||
52 | } | ||
53 | |||
54 | export type ActivityBitTorrentUrlObject = { | ||
55 | type: 'Link' | ||
56 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) | ||
57 | mimeType?: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet' | ||
58 | mediaType: 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet' | ||
59 | href: string | ||
60 | height: number | ||
61 | } | ||
62 | |||
63 | export type ActivityHtmlUrlObject = { | ||
64 | type: 'Link' | ||
65 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) | ||
66 | mimeType?: 'text/html' | ||
67 | mediaType: 'text/html' | ||
68 | href: string | ||
69 | } | ||
70 | |||
71 | export type ActivityUrlObject = ActivityVideoUrlObject | ActivityPlaylistUrlObject | ActivityBitTorrentUrlObject | ActivityHtmlUrlObject | ||
50 | 72 | ||
51 | export interface ActivityPubAttributedTo { | 73 | export interface ActivityPubAttributedTo { |
52 | type: 'Group' | 'Person' | 74 | type: 'Group' | 'Person' |
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts index 6b3b1b47c..a3953874d 100644 --- a/shared/models/actors/actor.model.ts +++ b/shared/models/actors/actor.model.ts | |||
@@ -10,5 +10,5 @@ export interface Actor { | |||
10 | followersCount: number | 10 | followersCount: number |
11 | createdAt: Date | string | 11 | createdAt: Date | string |
12 | updatedAt: Date | string | 12 | updatedAt: Date | string |
13 | avatar: Avatar | 13 | avatar?: Avatar |
14 | } | 14 | } |
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 7a3eaa33f..b42ff90c6 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -61,6 +61,9 @@ export interface CustomConfig { | |||
61 | '720p': boolean | 61 | '720p': boolean |
62 | '1080p': boolean | 62 | '1080p': boolean |
63 | } | 63 | } |
64 | hls: { | ||
65 | enabled: boolean | ||
66 | } | ||
64 | } | 67 | } |
65 | 68 | ||
66 | import: { | 69 | import: { |
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index f4245ed4d..baafed31f 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -25,11 +25,15 @@ export interface ServerConfig { | |||
25 | 25 | ||
26 | signup: { | 26 | signup: { |
27 | allowed: boolean, | 27 | allowed: boolean, |
28 | allowedForCurrentIP: boolean, | 28 | allowedForCurrentIP: boolean |
29 | requiresEmailVerification: boolean | 29 | requiresEmailVerification: boolean |
30 | } | 30 | } |
31 | 31 | ||
32 | transcoding: { | 32 | transcoding: { |
33 | hls: { | ||
34 | enabled: boolean | ||
35 | } | ||
36 | |||
33 | enabledResolutions: number[] | 37 | enabledResolutions: number[] |
34 | } | 38 | } |
35 | 39 | ||
@@ -48,7 +52,7 @@ export interface ServerConfig { | |||
48 | file: { | 52 | file: { |
49 | size: { | 53 | size: { |
50 | max: number | 54 | max: number |
51 | }, | 55 | } |
52 | extensions: string[] | 56 | extensions: string[] |
53 | } | 57 | } |
54 | } | 58 | } |
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts index f41b6f534..186b62612 100644 --- a/shared/models/users/user-notification.model.ts +++ b/shared/models/users/user-notification.model.ts | |||
@@ -22,16 +22,23 @@ export interface VideoInfo { | |||
22 | name: string | 22 | name: string |
23 | } | 23 | } |
24 | 24 | ||
25 | export interface ActorInfo { | ||
26 | id: number | ||
27 | displayName: string | ||
28 | name: string | ||
29 | host: string | ||
30 | avatar?: { | ||
31 | path: string | ||
32 | } | ||
33 | } | ||
34 | |||
25 | export interface UserNotification { | 35 | export interface UserNotification { |
26 | id: number | 36 | id: number |
27 | type: UserNotificationType | 37 | type: UserNotificationType |
28 | read: boolean | 38 | read: boolean |
29 | 39 | ||
30 | video?: VideoInfo & { | 40 | video?: VideoInfo & { |
31 | channel: { | 41 | channel: ActorInfo |
32 | id: number | ||
33 | displayName: string | ||
34 | } | ||
35 | } | 42 | } |
36 | 43 | ||
37 | videoImport?: { | 44 | videoImport?: { |
@@ -45,10 +52,7 @@ export interface UserNotification { | |||
45 | comment?: { | 52 | comment?: { |
46 | id: number | 53 | id: number |
47 | threadId: number | 54 | threadId: number |
48 | account: { | 55 | account: ActorInfo |
49 | id: number | ||
50 | displayName: string | ||
51 | } | ||
52 | video: VideoInfo | 56 | video: VideoInfo |
53 | } | 57 | } |
54 | 58 | ||
@@ -62,18 +66,11 @@ export interface UserNotification { | |||
62 | video: VideoInfo | 66 | video: VideoInfo |
63 | } | 67 | } |
64 | 68 | ||
65 | account?: { | 69 | account?: ActorInfo |
66 | id: number | ||
67 | displayName: string | ||
68 | name: string | ||
69 | } | ||
70 | 70 | ||
71 | actorFollow?: { | 71 | actorFollow?: { |
72 | id: number | 72 | id: number |
73 | follower: { | 73 | follower: ActorInfo |
74 | name: string | ||
75 | displayName: string | ||
76 | } | ||
77 | following: { | 74 | following: { |
78 | type: 'account' | 'channel' | 75 | type: 'account' | 'channel' |
79 | name: string | 76 | name: string |
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts index abde51321..cd215bab3 100644 --- a/shared/models/users/user-update.model.ts +++ b/shared/models/users/user-update.model.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { UserRole } from './user-role' | 1 | import { UserRole } from './user-role' |
2 | 2 | ||
3 | export interface UserUpdate { | 3 | export interface UserUpdate { |
4 | password?: string | ||
4 | email?: string | 5 | email?: string |
5 | emailVerified?: boolean | 6 | emailVerified?: boolean |
6 | videoQuota?: number | 7 | videoQuota?: number |
diff --git a/shared/models/videos/video-streaming-playlist.model.ts b/shared/models/videos/video-streaming-playlist.model.ts new file mode 100644 index 000000000..17f8fe865 --- /dev/null +++ b/shared/models/videos/video-streaming-playlist.model.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { VideoStreamingPlaylistType } from './video-streaming-playlist.type' | ||
2 | |||
3 | export class VideoStreamingPlaylist { | ||
4 | id: number | ||
5 | type: VideoStreamingPlaylistType | ||
6 | playlistUrl: string | ||
7 | segmentsSha256Url: string | ||
8 | |||
9 | redundancies: { | ||
10 | baseUrl: string | ||
11 | }[] | ||
12 | } | ||
diff --git a/shared/models/videos/video-streaming-playlist.type.ts b/shared/models/videos/video-streaming-playlist.type.ts new file mode 100644 index 000000000..3b403f295 --- /dev/null +++ b/shared/models/videos/video-streaming-playlist.type.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export enum VideoStreamingPlaylistType { | ||
2 | HLS = 1 | ||
3 | } | ||
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 4a9fa58b1..803db8255 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -5,6 +5,7 @@ import { VideoChannel } from './channel/video-channel.model' | |||
5 | import { VideoPrivacy } from './video-privacy.enum' | 5 | import { VideoPrivacy } from './video-privacy.enum' |
6 | import { VideoScheduleUpdate } from './video-schedule-update.model' | 6 | import { VideoScheduleUpdate } from './video-schedule-update.model' |
7 | import { VideoConstant } from './video-constant.model' | 7 | import { VideoConstant } from './video-constant.model' |
8 | import { VideoStreamingPlaylist } from './video-streaming-playlist.model' | ||
8 | 9 | ||
9 | export interface VideoFile { | 10 | export interface VideoFile { |
10 | magnetUri: string | 11 | magnetUri: string |
@@ -24,7 +25,7 @@ export interface VideoChannelAttribute { | |||
24 | displayName: string | 25 | displayName: string |
25 | url: string | 26 | url: string |
26 | host: string | 27 | host: string |
27 | avatar: Avatar | 28 | avatar?: Avatar |
28 | } | 29 | } |
29 | 30 | ||
30 | export interface AccountAttribute { | 31 | export interface AccountAttribute { |
@@ -34,7 +35,7 @@ export interface AccountAttribute { | |||
34 | displayName: string | 35 | displayName: string |
35 | url: string | 36 | url: string |
36 | host: string | 37 | host: string |
37 | avatar: Avatar | 38 | avatar?: Avatar |
38 | } | 39 | } |
39 | 40 | ||
40 | export interface Video { | 41 | export interface Video { |
@@ -86,4 +87,8 @@ export interface VideoDetails extends Video { | |||
86 | // Not optional in details (unlike in Video) | 87 | // Not optional in details (unlike in Video) |
87 | waitTranscoding: boolean | 88 | waitTranscoding: boolean |
88 | state: VideoConstant<VideoState> | 89 | state: VideoConstant<VideoState> |
90 | |||
91 | trackerUrls: string[] | ||
92 | |||
93 | streamingPlaylists: VideoStreamingPlaylist[] | ||
89 | } | 94 | } |
diff --git a/shared/utils/index.ts b/shared/utils/index.ts index e08bbfd2a..156901372 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts | |||
@@ -17,6 +17,8 @@ export * from './users/users' | |||
17 | export * from './videos/video-abuses' | 17 | export * from './videos/video-abuses' |
18 | export * from './videos/video-blacklist' | 18 | export * from './videos/video-blacklist' |
19 | export * from './videos/video-channels' | 19 | export * from './videos/video-channels' |
20 | export * from './videos/video-comments' | ||
21 | export * from './videos/video-playlists' | ||
20 | export * from './videos/videos' | 22 | export * from './videos/videos' |
21 | export * from './videos/video-change-ownership' | 23 | export * from './videos/video-change-ownership' |
22 | export * from './feeds/feeds' | 24 | export * from './feeds/feeds' |
diff --git a/shared/utils/requests/requests.ts b/shared/utils/requests/requests.ts index 77e9f6164..6b59e24fc 100644 --- a/shared/utils/requests/requests.ts +++ b/shared/utils/requests/requests.ts | |||
@@ -1,24 +1,32 @@ | |||
1 | import * as request from 'supertest' | 1 | import * as request from 'supertest' |
2 | import { buildAbsoluteFixturePath, root } from '../miscs/miscs' | 2 | import { buildAbsoluteFixturePath, root } from '../miscs/miscs' |
3 | import { isAbsolute, join } from 'path' | 3 | import { isAbsolute, join } from 'path' |
4 | import { parse } from 'url' | ||
5 | |||
6 | function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) { | ||
7 | const { host, protocol, pathname } = parse(url) | ||
8 | |||
9 | return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range }) | ||
10 | } | ||
4 | 11 | ||
5 | function makeGetRequest (options: { | 12 | function makeGetRequest (options: { |
6 | url: string, | 13 | url: string, |
7 | path: string, | 14 | path?: string, |
8 | query?: any, | 15 | query?: any, |
9 | token?: string, | 16 | token?: string, |
10 | statusCodeExpected?: number, | 17 | statusCodeExpected?: number, |
11 | contentType?: string | 18 | contentType?: string, |
19 | range?: string | ||
12 | }) { | 20 | }) { |
13 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 | 21 | if (!options.statusCodeExpected) options.statusCodeExpected = 400 |
14 | if (options.contentType === undefined) options.contentType = 'application/json' | 22 | if (options.contentType === undefined) options.contentType = 'application/json' |
15 | 23 | ||
16 | const req = request(options.url) | 24 | const req = request(options.url).get(options.path) |
17 | .get(options.path) | ||
18 | 25 | ||
19 | if (options.contentType) req.set('Accept', options.contentType) | 26 | if (options.contentType) req.set('Accept', options.contentType) |
20 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | 27 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) |
21 | if (options.query) req.query(options.query) | 28 | if (options.query) req.query(options.query) |
29 | if (options.range) req.set('Range', options.range) | ||
22 | 30 | ||
23 | return req.expect(options.statusCodeExpected) | 31 | return req.expect(options.statusCodeExpected) |
24 | } | 32 | } |
@@ -164,5 +172,6 @@ export { | |||
164 | makePostBodyRequest, | 172 | makePostBodyRequest, |
165 | makePutBodyRequest, | 173 | makePutBodyRequest, |
166 | makeDeleteRequest, | 174 | makeDeleteRequest, |
175 | makeRawRequest, | ||
167 | updateAvatarRequest | 176 | updateAvatarRequest |
168 | } | 177 | } |
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts index 0c5512bab..29c24cff9 100644 --- a/shared/utils/server/config.ts +++ b/shared/utils/server/config.ts | |||
@@ -97,6 +97,9 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) { | |||
97 | '480p': true, | 97 | '480p': true, |
98 | '720p': false, | 98 | '720p': false, |
99 | '1080p': false | 99 | '1080p': false |
100 | }, | ||
101 | hls: { | ||
102 | enabled: false | ||
100 | } | 103 | } |
101 | }, | 104 | }, |
102 | import: { | 105 | import: { |
diff --git a/shared/utils/server/servers.ts b/shared/utils/server/servers.ts index 1e9c83c72..bde7dd5c2 100644 --- a/shared/utils/server/servers.ts +++ b/shared/utils/server/servers.ts | |||
@@ -146,7 +146,11 @@ function runServer (serverNumber: number, configOverride?: Object, args = []) { | |||
146 | 146 | ||
147 | server.app.stdout.removeListener('data', onStdout) | 147 | server.app.stdout.removeListener('data', onStdout) |
148 | 148 | ||
149 | process.on('exit', () => process.kill(server.app.pid)) | 149 | process.on('exit', () => { |
150 | try { | ||
151 | process.kill(server.app.pid) | ||
152 | } catch { /* empty */ } | ||
153 | }) | ||
150 | 154 | ||
151 | res(server) | 155 | res(server) |
152 | }) | 156 | }) |
@@ -162,9 +166,13 @@ async function reRunServer (server: ServerInfo, configOverride?: any) { | |||
162 | } | 166 | } |
163 | 167 | ||
164 | async function checkTmpIsEmpty (server: ServerInfo) { | 168 | async function checkTmpIsEmpty (server: ServerInfo) { |
169 | return checkDirectoryIsEmpty(server, 'tmp') | ||
170 | } | ||
171 | |||
172 | async function checkDirectoryIsEmpty (server: ServerInfo, directory: string) { | ||
165 | const testDirectory = 'test' + server.serverNumber | 173 | const testDirectory = 'test' + server.serverNumber |
166 | 174 | ||
167 | const directoryPath = join(root(), testDirectory, 'tmp') | 175 | const directoryPath = join(root(), testDirectory, directory) |
168 | 176 | ||
169 | const directoryExists = existsSync(directoryPath) | 177 | const directoryExists = existsSync(directoryPath) |
170 | expect(directoryExists).to.be.true | 178 | expect(directoryExists).to.be.true |
@@ -195,6 +203,7 @@ async function waitUntilLog (server: ServerInfo, str: string, count = 1) { | |||
195 | // --------------------------------------------------------------------------- | 203 | // --------------------------------------------------------------------------- |
196 | 204 | ||
197 | export { | 205 | export { |
206 | checkDirectoryIsEmpty, | ||
198 | checkTmpIsEmpty, | 207 | checkTmpIsEmpty, |
199 | ServerInfo, | 208 | ServerInfo, |
200 | flushAndRunMultipleServers, | 209 | flushAndRunMultipleServers, |
diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts index bcbe29fc7..c8ed7df30 100644 --- a/shared/utils/users/user-notifications.ts +++ b/shared/utils/users/user-notifications.ts | |||
@@ -146,6 +146,7 @@ function checkVideo (video: any, videoName?: string, videoUUID?: string) { | |||
146 | function checkActor (actor: any) { | 146 | function checkActor (actor: any) { |
147 | expect(actor.displayName).to.be.a('string') | 147 | expect(actor.displayName).to.be.a('string') |
148 | expect(actor.displayName).to.not.be.empty | 148 | expect(actor.displayName).to.not.be.empty |
149 | expect(actor.host).to.not.be.undefined | ||
149 | } | 150 | } |
150 | 151 | ||
151 | function checkComment (comment: any, commentId: number, threadId: number) { | 152 | function checkComment (comment: any, commentId: number, threadId: number) { |
@@ -273,8 +274,8 @@ async function checkNewActorFollow ( | |||
273 | checkActor(notification.actorFollow.follower) | 274 | checkActor(notification.actorFollow.follower) |
274 | expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName) | 275 | expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName) |
275 | expect(notification.actorFollow.follower.name).to.equal(followerName) | 276 | expect(notification.actorFollow.follower.name).to.equal(followerName) |
277 | expect(notification.actorFollow.follower.host).to.not.be.undefined | ||
276 | 278 | ||
277 | checkActor(notification.actorFollow.following) | ||
278 | expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName) | 279 | expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName) |
279 | expect(notification.actorFollow.following.type).to.equal(followType) | 280 | expect(notification.actorFollow.following.type).to.equal(followType) |
280 | } else { | 281 | } else { |
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts index 61a7e3757..7191b263e 100644 --- a/shared/utils/users/users.ts +++ b/shared/utils/users/users.ts | |||
@@ -213,11 +213,13 @@ function updateUser (options: { | |||
213 | emailVerified?: boolean, | 213 | emailVerified?: boolean, |
214 | videoQuota?: number, | 214 | videoQuota?: number, |
215 | videoQuotaDaily?: number, | 215 | videoQuotaDaily?: number, |
216 | password?: string, | ||
216 | role?: UserRole | 217 | role?: UserRole |
217 | }) { | 218 | }) { |
218 | const path = '/api/v1/users/' + options.userId | 219 | const path = '/api/v1/users/' + options.userId |
219 | 220 | ||
220 | const toSend = {} | 221 | const toSend = {} |
222 | if (options.password !== undefined && options.password !== null) toSend['password'] = options.password | ||
221 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | 223 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email |
222 | if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified | 224 | if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified |
223 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota | 225 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota |
diff --git a/shared/utils/videos/video-playlists.ts b/shared/utils/videos/video-playlists.ts new file mode 100644 index 000000000..eb25011cb --- /dev/null +++ b/shared/utils/videos/video-playlists.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { makeRawRequest } from '../requests/requests' | ||
2 | import { sha256 } from '../../../server/helpers/core-utils' | ||
3 | import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model' | ||
4 | import { expect } from 'chai' | ||
5 | |||
6 | function getPlaylist (url: string, statusCodeExpected = 200) { | ||
7 | return makeRawRequest(url, statusCodeExpected) | ||
8 | } | ||
9 | |||
10 | function getSegment (url: string, statusCodeExpected = 200, range?: string) { | ||
11 | return makeRawRequest(url, statusCodeExpected, range) | ||
12 | } | ||
13 | |||
14 | function getSegmentSha256 (url: string, statusCodeExpected = 200) { | ||
15 | return makeRawRequest(url, statusCodeExpected) | ||
16 | } | ||
17 | |||
18 | async function checkSegmentHash ( | ||
19 | baseUrlPlaylist: string, | ||
20 | baseUrlSegment: string, | ||
21 | videoUUID: string, | ||
22 | resolution: number, | ||
23 | hlsPlaylist: VideoStreamingPlaylist | ||
24 | ) { | ||
25 | const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`) | ||
26 | const playlist = res.text | ||
27 | |||
28 | const videoName = `${videoUUID}-${resolution}-fragmented.mp4` | ||
29 | |||
30 | const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) | ||
31 | |||
32 | const length = parseInt(matches[1], 10) | ||
33 | const offset = parseInt(matches[2], 10) | ||
34 | const range = `${offset}-${offset + length - 1}` | ||
35 | |||
36 | const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`) | ||
37 | |||
38 | const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url) | ||
39 | |||
40 | const sha256Server = resSha.body[ videoName ][range] | ||
41 | expect(sha256(res2.body)).to.equal(sha256Server) | ||
42 | } | ||
43 | |||
44 | // --------------------------------------------------------------------------- | ||
45 | |||
46 | export { | ||
47 | getPlaylist, | ||
48 | getSegment, | ||
49 | getSegmentSha256, | ||
50 | checkSegmentHash | ||
51 | } | ||
diff --git a/shared/utils/videos/videos.ts b/shared/utils/videos/videos.ts index 0cf6e7c4f..b5b33e038 100644 --- a/shared/utils/videos/videos.ts +++ b/shared/utils/videos/videos.ts | |||
@@ -271,7 +271,16 @@ function removeVideo (url: string, token: string, id: number | string, expectedS | |||
271 | async function checkVideoFilesWereRemoved ( | 271 | async function checkVideoFilesWereRemoved ( |
272 | videoUUID: string, | 272 | videoUUID: string, |
273 | serverNumber: number, | 273 | serverNumber: number, |
274 | directories = [ 'redundancy', 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ] | 274 | directories = [ |
275 | 'redundancy', | ||
276 | 'videos', | ||
277 | 'thumbnails', | ||
278 | 'torrents', | ||
279 | 'previews', | ||
280 | 'captions', | ||
281 | join('playlists', 'hls'), | ||
282 | join('redundancy', 'hls') | ||
283 | ] | ||
275 | ) { | 284 | ) { |
276 | const testDirectory = 'test' + serverNumber | 285 | const testDirectory = 'test' + serverNumber |
277 | 286 | ||
@@ -279,7 +288,7 @@ async function checkVideoFilesWereRemoved ( | |||
279 | const directoryPath = join(root(), testDirectory, directory) | 288 | const directoryPath = join(root(), testDirectory, directory) |
280 | 289 | ||
281 | const directoryExists = existsSync(directoryPath) | 290 | const directoryExists = existsSync(directoryPath) |
282 | expect(directoryExists).to.be.true | 291 | if (!directoryExists) continue |
283 | 292 | ||
284 | const files = await readdir(directoryPath) | 293 | const files = await readdir(directoryPath) |
285 | for (const file of files) { | 294 | for (const file of files) { |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 535dfdd4c..f2bb945f9 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -1,7 +1,7 @@ | |||
1 | openapi: 3.0.0 | 1 | openapi: 3.0.0 |
2 | info: | 2 | info: |
3 | title: PeerTube | 3 | title: PeerTube |
4 | version: 1.1.0 | 4 | version: 1.2.0 |
5 | contact: | 5 | contact: |
6 | name: PeerTube Community | 6 | name: PeerTube Community |
7 | url: 'https://joinpeertube.org' | 7 | url: 'https://joinpeertube.org' |
diff --git a/support/nginx/peertube b/support/nginx/peertube index 914ca3741..fee0f5d1c 100644 --- a/support/nginx/peertube +++ b/support/nginx/peertube | |||
@@ -41,7 +41,7 @@ server { | |||
41 | # It might be nice to compress JSON, but leaving that out to protect against potential | 41 | # It might be nice to compress JSON, but leaving that out to protect against potential |
42 | # compression+encryption information leak attacks like BREACH. | 42 | # compression+encryption information leak attacks like BREACH. |
43 | gzip on; | 43 | gzip on; |
44 | gzip_types text/css text/html application/javascript; | 44 | gzip_types text/css application/javascript; |
45 | gzip_vary on; | 45 | gzip_vary on; |
46 | 46 | ||
47 | # Enable HSTS | 47 | # Enable HSTS |
@@ -158,4 +158,16 @@ server { | |||
158 | proxy_set_header Host $host; | 158 | proxy_set_header Host $host; |
159 | proxy_pass http://localhost:9000; | 159 | proxy_pass http://localhost:9000; |
160 | } | 160 | } |
161 | |||
162 | location /socket.io { | ||
163 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
164 | proxy_set_header Host $host; | ||
165 | |||
166 | proxy_pass http://localhost:9000; | ||
167 | |||
168 | # enable WebSockets | ||
169 | proxy_http_version 1.1; | ||
170 | proxy_set_header Upgrade $http_upgrade; | ||
171 | proxy_set_header Connection "upgrade"; | ||
172 | } | ||
161 | } | 173 | } |