aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md89
-rw-r--r--README.md2
-rw-r--r--client/package.json39
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.html2
-rw-r--r--client/src/app/+accounts/accounts.component.ts3
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html5
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts4
-rw-r--r--client/src/app/+admin/users/user-edit/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html14
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts11
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html21
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss22
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts64
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts14
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html2
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-history/my-account-history.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html10
-rw-r--r--client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss4
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html3
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html2
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss2
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html6
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss11
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html3
-rw-r--r--client/src/app/app.component.html4
-rw-r--r--client/src/app/core/confirm/index.ts1
-rw-r--r--client/src/app/core/core.module.ts4
-rw-r--r--client/src/app/core/server/server.service.ts5
-rw-r--r--client/src/app/header/header.component.html2
-rw-r--r--client/src/app/header/header.component.scss11
-rw-r--r--client/src/app/login/login.component.html3
-rw-r--r--client/src/app/menu/avatar-notification.component.scss17
-rw-r--r--client/src/app/menu/language-chooser.component.html2
-rw-r--r--client/src/app/menu/menu.component.scss4
-rw-r--r--client/src/app/search/search.component.scss4
-rw-r--r--client/src/app/shared/actor/actor.model.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss9
-rw-r--r--client/src/app/shared/buttons/button.component.html2
-rw-r--r--client/src/app/shared/buttons/button.component.scss35
-rw-r--r--client/src/app/shared/buttons/button.component.ts3
-rw-r--r--client/src/app/shared/buttons/delete-button.component.html2
-rw-r--r--client/src/app/shared/buttons/edit-button.component.html2
-rw-r--r--client/src/app/shared/confirm/confirm.component.html (renamed from client/src/app/core/confirm/confirm.component.html)3
-rw-r--r--client/src/app/shared/confirm/confirm.component.scss (renamed from client/src/app/core/confirm/confirm.component.scss)0
-rw-r--r--client/src/app/shared/confirm/confirm.component.ts (renamed from client/src/app/core/confirm/confirm.component.ts)2
-rw-r--r--client/src/app/shared/icons/global-icon.component.html0
-rw-r--r--client/src/app/shared/icons/global-icon.component.scss4
-rw-r--r--client/src/app/shared/icons/global-icon.component.ts48
-rw-r--r--client/src/app/shared/misc/help.component.html4
-rw-r--r--client/src/app/shared/misc/help.component.scss27
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.html7
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts4
-rw-r--r--client/src/app/shared/shared.module.ts8
-rw-r--r--client/src/app/shared/users/user-notification.model.ts42
-rw-r--r--client/src/app/shared/users/user-notification.service.ts2
-rw-r--r--client/src/app/shared/users/user-notifications.component.html82
-rw-r--r--client/src/app/shared/users/user-notifications.component.scss46
-rw-r--r--client/src/app/shared/users/user-notifications.component.ts14
-rw-r--r--client/src/app/shared/video/feed.component.html9
-rw-r--r--client/src/app/shared/video/feed.component.scss17
-rw-r--r--client/src/app/shared/video/video-details.model.ts13
-rw-r--r--client/src/app/shared/video/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/video/video.model.ts4
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss28
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss53
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.scss (renamed from client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss)29
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss44
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.scss6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-blacklist.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-download.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-share.component.html2
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.html2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html24
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss80
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts113
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts25
-rw-r--r--client/src/assets/images/global/add.html (renamed from client/src/assets/images/global/add.svg)6
-rw-r--r--client/src/assets/images/global/alert.html (renamed from client/src/assets/images/video/alert.svg)9
-rw-r--r--client/src/assets/images/global/circle-tick.html12
-rw-r--r--client/src/assets/images/global/cloud-download.html11
-rw-r--r--client/src/assets/images/global/cloud-error.html11
-rw-r--r--client/src/assets/images/global/cog.html9
-rw-r--r--client/src/assets/images/global/cross.html (renamed from client/src/assets/images/global/cross.svg)6
-rw-r--r--client/src/assets/images/global/delete-black.svg14
-rw-r--r--client/src/assets/images/global/delete-grey.svg14
-rw-r--r--client/src/assets/images/global/delete.html (renamed from client/src/assets/images/global/delete-white.svg)14
-rw-r--r--client/src/assets/images/global/download.html (renamed from client/src/assets/images/video/download-black.svg)9
-rw-r--r--client/src/assets/images/global/edit-black.svg15
-rw-r--r--client/src/assets/images/global/edit.html (renamed from client/src/assets/images/global/edit-grey.svg)9
-rw-r--r--client/src/assets/images/global/help.html (renamed from client/src/assets/images/global/help.svg)12
-rw-r--r--client/src/assets/images/global/im-with-her.html (renamed from client/src/assets/images/global/im-with-her.svg)13
-rw-r--r--client/src/assets/images/global/no.html10
-rw-r--r--client/src/assets/images/global/sparkle.html11
-rw-r--r--client/src/assets/images/global/syndication.html (renamed from client/src/assets/images/global/syndication.svg)4
-rw-r--r--client/src/assets/images/global/tick.html (renamed from client/src/assets/images/global/tick.svg)6
-rw-r--r--client/src/assets/images/global/undo.html9
-rw-r--r--client/src/assets/images/global/undo.svg11
-rw-r--r--client/src/assets/images/global/user-add.html11
-rw-r--r--client/src/assets/images/global/validate.html (renamed from client/src/assets/images/global/validate.svg)6
-rw-r--r--client/src/assets/images/video/blacklist.svg15
-rw-r--r--client/src/assets/images/video/dislike-white.svg14
-rw-r--r--client/src/assets/images/video/dislike.html (renamed from client/src/assets/images/video/dislike-grey.svg)6
-rw-r--r--client/src/assets/images/video/download-grey.svg16
-rw-r--r--client/src/assets/images/video/download-white.svg16
-rw-r--r--client/src/assets/images/video/heart.html (renamed from client/src/assets/images/video/heart.svg)8
-rw-r--r--client/src/assets/images/video/like-white.svg15
-rw-r--r--client/src/assets/images/video/like.html (renamed from client/src/assets/images/video/like-grey.svg)9
-rw-r--r--client/src/assets/images/video/more.html (renamed from client/src/assets/images/video/more.svg)6
-rw-r--r--client/src/assets/images/video/share.html (renamed from client/src/assets/images/video/share.svg)9
-rw-r--r--client/src/assets/images/video/upload.html (renamed from client/src/assets/images/header/upload-white.svg)9
-rw-r--r--client/src/assets/images/video/upload.svg16
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts143
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-url-builder.ts28
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-validator.ts63
-rw-r--r--client/src/assets/player/peertube-player-manager.ts466
-rw-r--r--client/src/assets/player/peertube-player.ts300
-rw-r--r--client/src/assets/player/peertube-plugin.ts262
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts92
-rw-r--r--client/src/assets/player/resolution-menu-button.ts88
-rw-r--r--client/src/assets/player/resolution-menu-item.ts67
-rw-r--r--client/src/assets/player/utils.ts14
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts (renamed from client/src/assets/player/webtorrent-info-button.ts)25
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts (renamed from client/src/assets/player/peertube-link-button.ts)4
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts (renamed from client/src/assets/player/peertube-load-progress-bar.ts)4
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts109
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts83
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts (renamed from client/src/assets/player/settings-menu-button.ts)4
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts (renamed from client/src/assets/player/settings-menu-item.ts)21
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts (renamed from client/src/assets/player/theater-button.ts)4
-rw-r--r--client/src/assets/player/webtorrent/peertube-chunk-store.ts (renamed from client/src/assets/player/peertube-chunk-store.ts)0
-rw-r--r--client/src/assets/player/webtorrent/video-renderer.ts (renamed from client/src/assets/player/video-renderer.ts)0
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts (renamed from client/src/assets/player/peertube-videojs-plugin.ts)305
-rw-r--r--client/src/index.html2
-rw-r--r--client/src/main.ts2
-rw-r--r--client/src/sass/application.scss13
-rw-r--r--client/src/sass/include/_mixins.scss54
-rw-r--r--client/src/sass/include/_variables.scss9
-rw-r--r--client/src/sass/primeng-custom.scss71
-rw-r--r--client/src/standalone/videos/embed.html1
-rw-r--r--client/src/standalone/videos/embed.ts128
-rw-r--r--client/src/tsconfig.app.json2
-rw-r--r--client/tsconfig.json1
-rw-r--r--client/yarn.lock1180
-rw-r--r--config/default.yaml9
-rw-r--r--config/production.yaml.example9
-rw-r--r--config/test-1.yaml1
-rw-r--r--config/test-2.yaml1
-rw-r--r--config/test-3.yaml1
-rw-r--r--config/test-4.yaml1
-rw-r--r--config/test-5.yaml1
-rw-r--r--config/test-6.yaml1
-rw-r--r--config/test.yaml2
-rw-r--r--package.json2
-rwxr-xr-xscripts/dev/server.sh2
-rwxr-xr-xscripts/generate-code-contributors.ts2
-rwxr-xr-xscripts/i18n/create-custom-files.ts5
-rwxr-xr-xscripts/update-host.ts14
-rw-r--r--server/controllers/activitypub/client.ts15
-rw-r--r--server/controllers/api/config.ts8
-rw-r--r--server/controllers/api/users/index.ts19
-rw-r--r--server/controllers/api/users/me.ts2
-rw-r--r--server/controllers/api/videos/index.ts19
-rw-r--r--server/controllers/static.ts9
-rw-r--r--server/controllers/tracker.ts25
-rw-r--r--server/helpers/activitypub.ts5
-rw-r--r--server/helpers/core-utils.ts8
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts12
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts8
-rw-r--r--server/helpers/custom-validators/misc.ts5
-rw-r--r--server/helpers/custom-validators/videos.ts4
-rw-r--r--server/helpers/ffmpeg-utils.ts38
-rw-r--r--server/helpers/requests.ts2
-rw-r--r--server/helpers/utils.ts1
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/initializers/checker-before-init.ts2
-rw-r--r--server/initializers/constants.ts20
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/initializers/installer.ts5
-rw-r--r--server/initializers/migrations/0330-video-streaming-playlist.ts51
-rw-r--r--server/lib/activitypub/actor.ts4
-rw-r--r--server/lib/activitypub/cache-file.ts23
-rw-r--r--server/lib/activitypub/send/send-create.ts7
-rw-r--r--server/lib/activitypub/send/send-undo.ts3
-rw-r--r--server/lib/activitypub/send/send-update.ts2
-rw-r--r--server/lib/activitypub/url.ts7
-rw-r--r--server/lib/activitypub/videos.ts97
-rw-r--r--server/lib/emailer.ts4
-rw-r--r--server/lib/hls.ts164
-rw-r--r--server/lib/job-queue/handlers/video-file.ts69
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts189
-rw-r--r--server/lib/video-transcoding.ts52
-rw-r--r--server/middlewares/csp.ts2
-rw-r--r--server/middlewares/validators/redundancy.ts33
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/models/account/user-notification.ts171
-rw-r--r--server/models/redundancy/video-redundancy.ts139
-rw-r--r--server/models/video/video-file.ts6
-rw-r--r--server/models/video/video-format-utils.ts61
-rw-r--r--server/models/video/video-streaming-playlist.ts158
-rw-r--r--server/models/video/video.ts179
-rw-r--r--server/tests/api/check-params/accounts.ts2
-rw-r--r--server/tests/api/check-params/config.ts3
-rw-r--r--server/tests/api/check-params/users.ts18
-rw-r--r--server/tests/api/redundancy/redundancy.ts216
-rw-r--r--server/tests/api/server/config.ts6
-rw-r--r--server/tests/api/users/user-notifications.ts36
-rw-r--r--server/tests/api/users/users.ts16
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/video-hls.ts139
-rw-r--r--server/tests/cli/update-host.ts11
-rw-r--r--shared/models/activitypub/activitypub-ordered-collection.ts5
-rw-r--r--shared/models/activitypub/objects/cache-file-object.ts4
-rw-r--r--shared/models/activitypub/objects/common-objects.ts60
-rw-r--r--shared/models/actors/actor.model.ts2
-rw-r--r--shared/models/server/custom-config.model.ts3
-rw-r--r--shared/models/server/server-config.model.ts8
-rw-r--r--shared/models/users/user-notification.model.ts31
-rw-r--r--shared/models/users/user-update.model.ts1
-rw-r--r--shared/models/videos/video-streaming-playlist.model.ts12
-rw-r--r--shared/models/videos/video-streaming-playlist.type.ts3
-rw-r--r--shared/models/videos/video.model.ts9
-rw-r--r--shared/utils/index.ts2
-rw-r--r--shared/utils/requests/requests.ts17
-rw-r--r--shared/utils/server/config.ts3
-rw-r--r--shared/utils/server/servers.ts13
-rw-r--r--shared/utils/users/user-notifications.ts3
-rw-r--r--shared/utils/users/users.ts2
-rw-r--r--shared/utils/videos/video-playlists.ts51
-rw-r--r--shared/utils/videos/videos.ts13
-rw-r--r--support/doc/api/openapi.yaml2
-rw-r--r--support/nginx/peertube14
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
diff --git a/README.md b/README.md
index 7b2460f1e..a9b4eb54a 100644
--- a/README.md
+++ b/README.md
@@ -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.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { 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 @@
1export * from './user-create.component' 1export * from './user-create.component'
2export * from './user-update.component' 2export * from './user-update.component'
3export * 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
17input[type=submit] { 17input[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
4input: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
13input[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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { UserService } from '@app/shared/users/user.service'
4import { Notifier } from '../../../core'
5import { User, UserUpdate } from '../../../../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
9import { FormReactive } from '../../../shared'
10
11@Component({
12 selector: 'my-user-password',
13 templateUrl: './user-password.component.html',
14 styleUrls: [ './user-password.component.scss' ]
15})
16export 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'
19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { 19export 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
8tr.banned { 8tr.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>&nbsp;-&nbsp; 32 <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a>&nbsp;-&nbsp;
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 @@
1export * from './confirm.component'
2export * 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'
8import { LoadingBarRouterModule } from '@ngx-loading-bar/router' 8import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
9 9
10import { AuthService } from './auth' 10import { AuthService } from './auth'
11import { ConfirmComponent, ConfirmService } from './confirm' 11import { ConfirmService } from './confirm'
12import { throwIfAlreadyLoaded } from './module-import-guard' 12import { throwIfAlreadyLoaded } from './module-import-guard'
13import { LoginGuard, RedirectService, UserRightGuard } from './routing' 13import { LoginGuard, RedirectService, UserRightGuard } from './routing'
14import { ServerService } from './server' 14import { 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 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { 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'
9export class ButtonComponent { 10export 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 @@
1import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'
2import { ConfirmService } from './confirm.service' 2import { ConfirmService } from '@app/core/confirm/confirm.service'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { 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 @@
1import { Component, ElementRef, Input, OnInit } from '@angular/core'
2
3const 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
31export type GlobalIconName = keyof typeof icons
32
33@Component({
34 selector: 'my-global-icon',
35 template: '',
36 styleUrls: [ './global-icon.component.scss' ]
37})
38export 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
67import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' 67import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
68import { InstanceService } from '@app/shared/instance/instance.service' 68import { InstanceService } from '@app/shared/instance/instance.service'
69import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' 69import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
70import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
71import { 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 @@
1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared' 1import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared'
2import { Actor } from '@app/shared/actor/actor.model'
2 3
3export class UserNotification implements UserNotificationServer { 4export 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'
3import { Video } from '../../shared/video/video.model' 3import { Video } from '../../shared/video/video.model'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 5import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
6import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model'
7import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type'
6 8
7export class VideoDetails extends Video implements VideoDetailsServerModel { 9export 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})
24export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 25export 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})
24export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 24export 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})
26export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 27export 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
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10// FIXME: something weird with our path definition in tsconfig and typings
11// @ts-ignore
12import videojs from 'video.js'
13import 'videojs-hotkeys'
14import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
15import * as WebTorrent from 'webtorrent'
16import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 11import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
17import '../../../assets/player/peertube-videojs-plugin'
18import { AuthService, ConfirmService } from '../../core' 12import { AuthService, ConfirmService } from '../../core'
19import { RestExtractor, VideoBlacklistService } from '../../shared' 13import { RestExtractor, VideoBlacklistService } from '../../shared'
20import { VideoDetails } from '../../shared/video/video-details.model' 14import { VideoDetails } from '../../shared/video/video-details.model'
@@ -24,12 +18,16 @@ import { VideoReportComponent } from './modal/video-report.component'
24import { VideoShareComponent } from './modal/video-share.component' 18import { VideoShareComponent } from './modal/video-share.component'
25import { VideoBlacklistComponent } from './modal/video-blacklist.component' 19import { VideoBlacklistComponent } from './modal/video-blacklist.component'
26import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 20import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
27import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
28import { I18n } from '@ngx-translate/i18n-polyfill' 21import { I18n } from '@ngx-translate/i18n-polyfill'
29import { environment } from '../../../environments/environment' 22import { environment } from '../../../environments/environment'
30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
31import { VideoCaptionService } from '@app/shared/video-caption' 23import { VideoCaptionService } from '@app/shared/video-caption'
32import { MarkdownService } from '@app/shared/renderer' 24import { MarkdownService } from '@app/shared/renderer'
25import {
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
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events } from 'p2p-media-loader-core'
7
8// videojs-hlsjs-plugin needs videojs in window
9window['videojs'] = videojs
10require('@streamroot/videojs-hlsjs-plugin')
11
12const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
13class 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
142videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
143export { 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 @@
1import { basename } from 'path'
2import { Segment } from 'p2p-media-loader-core'
3
4function 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
20export {
21 segmentUrlBuilderFactory
22}
23
24// ---------------------------------------------------------------------------
25
26function 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 @@
1import { Segment } from 'p2p-media-loader-core'
2import { basename } from 'path'
3
4function 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
31export {
32 segmentValidatorFactory
33}
34
35// ---------------------------------------------------------------------------
36
37function 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
46function 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
53function 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 @@
1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore
3import * as videojs from 'video.js'
4import 'videojs-hotkeys'
5import 'videojs-dock'
6import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels'
8import './peertube-plugin'
9import './videojs-components/peertube-link-button'
10import './videojs-components/resolution-menu-button'
11import './videojs-components/settings-menu-button'
12import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-load-progress-bar'
14import './videojs-components/theater-button'
15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
16import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
17import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
18import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
19import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.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)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
29
30export type WebtorrentOptions = {
31 videoFiles: VideoFile[]
32}
33
34export type P2PMediaLoaderOptions = {
35 playlistUrl: string
36 segmentsSha256Url: string
37 trackerAnnounce: string[]
38 redundancyBaseUrls: string[]
39 videoFiles: VideoFile[]
40}
41
42export 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
73export type PeertubePlayerManagerOptions = {
74 common: CommonOptions,
75 webtorrent: WebtorrentOptions,
76 p2pMediaLoader?: P2PMediaLoaderOptions
77}
78
79export 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
464export {
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 @@
1import { VideoFile } from '../../../../shared/models/videos'
2
3import 'videojs-hotkeys'
4import 'videojs-dock'
5import 'videojs-contextmenu-ui'
6import './peertube-link-button'
7import './resolution-menu-button'
8import './settings-menu-button'
9import './webtorrent-info-button'
10import './peertube-videojs-plugin'
11import './peertube-load-progress-bar'
12import './theater-button'
13import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
14import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
15import { 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
19import { Player } from 'video.js'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.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)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28function 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
144function 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
208function 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
241function 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}
266namespace loadLocaleInVideoJS {
267 export const cache: { [ path: string ]: any } = {}
268}
269
270function 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
285export {
286 getServerTranslations,
287 loadLocaleInVideoJS,
288 getVideojsOptions,
289 addContextMenu
290}
291
292// ############################################################################
293
294function 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
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button'
5import {
6 PeerTubePluginOptions,
7 ResolutionUpdateData,
8 UserWatching,
9 VideoJSCaption,
10 VideoJSComponentInterface,
11 videojsUntyped
12} from './peertube-videojs-typings'
13import { isMobile, timeToInt } from './utils'
14import {
15 getStoredLastSubtitle,
16 getStoredMute,
17 getStoredVolume,
18 saveLastSubtitle,
19 saveMuteInStore,
20 saveVolumeInStore
21} from './peertube-player-local-storage'
22
23const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
24class 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
261videojs.registerPlugin('peertube', PeerTubePlugin)
262export { 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 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
6import { PeerTubePlugin } from './peertube-videojs-plugin' 6import { PeerTubePlugin } from './peertube-plugin'
7import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
8import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
9import { PlayerMode } from './peertube-player-manager'
7 10
8declare namespace videojs { 11declare 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
33type PeertubePluginOptions = { 38type 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
52type WebtorrentPluginOptions = {
53 playerElement: HTMLVideoElement
54
55 autoplay: boolean
56 videoDuration: number
57
58 videoFiles: VideoFile[]
59}
60
61type P2PMediaLoaderPluginOptions = {
62 redundancyBaseUrls: string[]
63 type: string
64 src: string
65}
66
67type 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
47const videojsUntyped = videojs as any 76const videojsUntyped = videojs as any
48 77
78type LoadedQualityData = {
79 qualitySwitchCallback: Function,
80 qualityData: {
81 video: {
82 id: number
83 label: string
84 selected: boolean
85 }[]
86 }
87}
88
89type ResolutionUpdateData = {
90 auto: boolean,
91 resolutionId: number
92 id?: number
93}
94
95type AutoResolutionUpdateData = {
96 possible: boolean
97}
98
99type 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
49export { 116export {
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
3import { Player } from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class 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}
86ResolutionMenuButton.prototype.controlText_ = 'Quality'
87
88MenuButton.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
3import { Player } from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class 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}
65MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
66
67export { 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
115function 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
117export { 130export {
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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { bytes } from './utils' 2import { bytes } from '../utils'
3 3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class WebtorrentInfoButton extends Button { 5class 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}
102Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) 105Button.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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from './utils' 2import { 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
5import { Player } from 'video.js' 5import { 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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { 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
4import { Player } from 'video.js' 4import { 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
3import { Player } from 'video.js'
4
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class 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}
107ResolutionMenuButton.prototype.controlText_ = 'Quality'
108
109MenuButton.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
3import { Player } from 'video.js'
4
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class 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}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82
83export { 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 @@
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { SettingsMenuItem } from './settings-menu-item' 8import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from './utils' 10import { toTitleCase } from '../utils'
11 11
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 13const 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
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { toTitleCase } from './utils' 8import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10 10
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const 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
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' 6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7 7
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
9class TheaterButton extends Button { 9class 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 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import * as WebTorrent from 'webtorrent' 5import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 6import { VideoFile } from '../../../../../shared/models/videos/video.model'
7import { renderVideo } from './video-renderer' 7import { renderVideo } from './video-renderer'
8import './settings-menu-button' 8import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
9import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
10import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
11import { PeertubeChunkStore } from './peertube-chunk-store' 10import { PeertubeChunkStore } from './peertube-chunk-store'
12import { 11import {
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
24const CacheChunkStore = require('cache-chunk-store') 19const CacheChunkStore = require('cache-chunk-store')
25 20
@@ -30,14 +25,13 @@ type PlayOptions = {
30} 25}
31 26
32const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
33class PeerTubePlugin extends Plugin { 28class 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
753videojs.registerPlugin('peertube', PeerTubePlugin) 638videojs.registerPlugin('webtorrent', WebTorrentPlugin)
754export { PeerTubePlugin } 639export { 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 {
324table { 327table {
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 @@
14p-table { 14p-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
18import 'whatwg-fetch' 18import 'whatwg-fetch'
19 19
20// FIXME: something weird with our path definition in tsconfig and typings
21// @ts-ignore
22import * as vjs from 'video.js'
23
24import * as Channel from 'jschannel' 20import * as Channel from 'jschannel'
25 21
26import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' 22import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
27import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player'
28import { PeerTubeResolution } from '../player/definitions' 23import { PeerTubeResolution } from '../player/definitions'
29import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 24import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
30import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 25import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
26import {
27 P2PMediaLoaderOptions,
28 PeertubePlayerManager,
29 PeertubePlayerManagerOptions,
30 PlayerMode
31} from '../../assets/player/peertube-player-manager'
32import { 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
865abbrev@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
870accepts@~1.3.4, accepts@~1.3.5: 748accepts@~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
878acorn-dynamic-import@^3.0.0: 756acorn-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
885acorn-globals@^4.1.0: 761acorn-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
898acorn@^5.0.0, acorn@^5.5.3, acorn@^5.6.2, acorn@^5.7.3: 774acorn@^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
784acorn@^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
908addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.4.2: 789addr-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
956ajv@6.5.3: 837ajv@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
913ansi-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
1032ansi-styles@^2.2.1: 918ansi-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
1241async@1.x, async@^1.5.2: 1127async@^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
1263autoprefixer@9.3.1: 1149autoprefixer@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
1275awesome-typescript-loader@5.2.1: 1161awesome-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
1425big.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
1539binary-extensions@^1.0.0: 1430binary-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
1591bittorrent-tracker@^9.0.0: 1482bittorrent-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
1656bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.2: 1547bluebird@^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
1825browserslist@^4.3.3: 1716browserslist@^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
1834browserstack@^1.5.1: 1725browserstack@^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
1956cacache@^11.0.1, cacache@^11.0.2, cacache@^11.2.0: 1847cacache@^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
1867cacache@^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
1976cache-base@^1.0.1: 1887cache-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
2044caniuse-lite@^1.0.30000898, caniuse-lite@^1.0.30000912: 1955caniuse-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
2049canonical-path@1.0.0: 1960canonical-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
1997chalk@^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
2086chardet@^0.7.0: 2006chardet@^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
2323commander@~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
2328commondir@^1.0.1: 2243commondir@^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
2476copy-webpack-plugin@4.5.4: 2391copy-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
2692debug@^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
2777decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: 2699decamelize@^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
2947detect-node@^2.0.3: 2869detect-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
3144electron-to-chromium@^1.3.86: 3066electron-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
3149elliptic@^6.0.0: 3071elliptic@^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
3362escodegen@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
3374escodegen@^1.9.1: 3284escodegen@^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
3394esprima@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
3399esprima@^3.1.3, esprima@~3.1.0: 3304esprima@^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
3416estraverse@^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
3421estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: 3321estraverse@^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
3441eventemitter3@^3.0.0: 3341eventemitter3@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
3351events@^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
3451eventsource@^1.0.7: 3356eventsource@^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
3724file-loader@2.0.0, file-loader@^2.0.0: 3629file-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
3637file-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
4046get-browser-rtc@^1.0.0: 3959get-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
4149glob@^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
4160global-modules-path@^2.3.0: 4062global-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
4227graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: 4129graceful-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
4245handle-thing@^1.2.5: 4147handle-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
4250handlebars@^4.0.1, handlebars@^4.0.11, handlebars@^4.0.3: 4152handlebars@^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
4274hls.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
4372hmac-drbg@^1.0.0: 4282hmac-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
4704inquirer@6.2.0: 4614inquirer@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
4723internal-ip@^3.0.1: 4633internal-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
5269istanbul@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
5289jasmine-core@^3.1.0: 5179jasmine-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
5671js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: 5561js-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
5675json5@^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
5785jsonfile@^4.0.0: 5682jsonfile@^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
5997less@3.8.1: 5894less@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
6026license-webpack-plugin@2.0.2: 5923license-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
6033lie@~3.1.0: 5931lie@~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
6093loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0: 5991loader-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
6102loader-utils@^0.2.16: 6000loader-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
6010loader-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
6112locate-path@^2.0.0: 6019locate-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
6149lru-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
6242lru@^3.0.0, lru@^3.1.0: 6156lru@^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
6168m3u8-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
6254magic-string@^0.25.0: 6175magic-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
6535mini-css-extract-plugin@0.4.4: 6456mini-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: 6475minimatch@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
6946node-releases@^1.0.5: 6867node-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
6953node-sass@4.10.0, node-sass@^4.9.3: 6874node-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
6899node-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
7025npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: 6971npm-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
7043npm-pick-manifest@^2.1.0: 6989npm-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
7177obuf@^1.0.0, obuf@^1.1.1: 7123obuf@^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
7194once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: 7140once@^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
7213opn@5.3.0: 7159opn@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
7220opn@^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
7299p2p-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
7310p2p-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
7360package-json-versionify@^1.0.2: 7319package-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
7367pacote@9.1.1: 7326pacote@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
7661portfinder@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
7670portfinder@^1.0.9: 7620portfinder@^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
7684postcss-import@12.0.0: 7634postcss-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
7748postcss@7.0.5: 7698postcss@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
7757postcss@^6.0.1, postcss@^6.0.23: 7707postcss@^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
7766postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.5: 7716postcss@^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
7956punycode@1.3.2: 7906punycode@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
7961punycode@^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
7966punycode@^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
7971purify-css@^1.2.5: 7911purify-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
8101raw-loader@0.5.1, raw-loader@^0.5.1: 8041raw-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
8049raw-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
8456resolve@1.1.7, resolve@1.1.x: 8404resolve@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
8753semver@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
8758semver@~5.3.0: 8701semver@~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
8845sha.js@^2.4.0, sha.js@^2.4.8: 8788sha.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
9121source-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: 9064source-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
9079source-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
9136source-map-url@^0.4.0: 9087source-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
9175source-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
9182sourcemap-codec@^1.4.1: 9126sourcemap-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
9213spdy-transport@^2.0.18: 9157spdy-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
9226spdy@^3.4.1: 9169spdy@^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
9238speed-measure-webpack-plugin@1.2.3: 9180speed-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
9439strip-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
9497strip-bom@3.0.0, strip-bom@^3.0.0: 9446strip-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
9560supports-color@^3.1.0, supports-color@^3.1.2: 9509supports-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
9523supports-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
9574symbol-observable@1.2.0: 9530symbol-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
9598tar@^4, tar@^4.4.6: 9554tar@^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
9611terser-webpack-plugin@1.1.0, terser-webpack-plugin@^1.1.0: 9567terser-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
9581terser-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
9595terser@^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
9625terser@^3.8.1: 9604terser@^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
9787tree-kill@1.2.0: 9766tree-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
9792trim-newlines@^1.0.0: 9771trim-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
9935typescript@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
9956uc.micro@^1.0.1, uc.micro@^1.0.5: 9940uc.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
9961uglify-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
9969uglify-js@3.4.x, uglify-js@^3.0.6, uglify-js@^3.1.4: 9945uglify-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
9977uglifyjs-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
9991uint64be@^2.0.2: 9953uint64be@^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
10085url-toolkit@^2.1.1, url-toolkit@^2.1.3: 10047url-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
10198videojs-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
10236videojs-dock@^2.0.2: 10206videojs-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
10318wbuf@^1.1.0, wbuf@^1.7.2: 10288wbuf@^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
10407webpack-dev-server@3.1.10: 10377webpack-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
10387webpack-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
10459webpack-merge@4.1.4: 10441webpack-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
10466webpack-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
10474webpack-sources@1.3.0, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0: 10448webpack-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
10497webpack@4.23.1: 10471webpack@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
10527webpack@^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
10665which@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: 10609which@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
10679wordwrap@^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
10684wordwrap@~0.0.2: 10623wordwrap@~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
10628wordwrap@~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
10689worker-farm@^1.5.2: 10633worker-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
142import: 151import:
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
155import: 164import:
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
66import: 68import:
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
5if [ ! -f "./client/dist/en_US/index.html" ]; then 5if [ ! -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
8fi 8fi
9 9
10npm run watch:server 10npm 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
43function get (url: string, headers: any = {}) { 43function 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}
33const playerTranslations = { 36const 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'
13import { getServerActor } from '../server/helpers/utils' 13import { getServerActor } from '../server/helpers/utils'
14import { AccountModel } from '../server/models/account/account' 14import { AccountModel } from '../server/models/account/account'
15import { VideoChannelModel } from '../server/models/video/video-channel' 15import { VideoChannelModel } from '../server/models/video/video-channel'
16import { VideoStreamingPlaylistModel } from '../server/models/video/video-streaming-playlist'
16 17
17run() 18run()
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'
35import { VideoCaptionModel } from '../../models/video/video-caption' 35import { VideoCaptionModel } from '../../models/video/video-caption'
36import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' 36import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
37import { getServerActor } from '../../helpers/utils' 37import { getServerActor } from '../../helpers/utils'
38import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 38import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
39import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' 39import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
@@ -63,11 +63,11 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
63 63
64activityPubClientRouter.get('/videos/watch/:id', 64activityPubClientRouter.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)
69activityPubClientRouter.get('/videos/watch/:id/activity', 69activityPubClientRouter.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)
73activityPubClientRouter.get('/videos/watch/:id/announces', 73activityPubClientRouter.get('/videos/watch/:id/announces',
@@ -113,7 +113,11 @@ activityPubClientRouter.get('/video-channels/:name/following',
113) 113)
114 114
115activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', 115activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?',
116 executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), 116 executeIfActivityPub(asyncMiddleware(videoFileRedundancyGetValidator)),
117 executeIfActivityPub(asyncMiddleware(videoRedundancyController))
118)
119activityPubClientRouter.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
162async function videoController (req: express.Request, res: express.Response) { 166async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { omit, snakeCase } from 'lodash' 2import { snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 3import { ServerConfig, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 4import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { 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
232async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 232async 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
241function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 241function 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
245async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 245async 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
251async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 251async 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
257async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { 257async 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
267async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 267async 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
170async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { 170async 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)
124videosRouter.get('/:id', 125videosRouter.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)
130videosRouter.post('/:id/views', 131videosRouter.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
398function getVideo (req: express.Request, res: express.Response) { 399async 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
409async function viewVideo (req: express.Request, res: express.Response) { 412async 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 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' 3import { CONFIG, HLS_PLAYLIST_DIRECTORY, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
4import { VideosPreviewCache } from '../lib/cache' 4import { VideosPreviewCache } from '../lib/cache'
5import { cacheRoute } from '../middlewares/cache' 5import { cacheRoute } from '../middlewares/cache'
6import { asyncMiddleware, videosGetValidator } from '../middlewares' 6import { asyncMiddleware, videosGetValidator } from '../middlewares'
@@ -51,6 +51,13 @@ staticRouter.use(
51 asyncMiddleware(downloadVideoFile) 51 asyncMiddleware(downloadVideoFile)
52) 52)
53 53
54// HLS
55staticRouter.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
55const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR 62const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
56staticRouter.use( 63staticRouter.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'
7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' 7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants'
8import { VideoFileModel } from '../models/video/video-file' 8import { VideoFileModel } from '../models/video/video-file'
9import { parse } from 'url' 9import { parse } from 'url'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
10 11
11const TrackerServer = bitTorrentTracker.Server 12const 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
196function sha256 (str: string, encoding: HexBase64Latin1Encoding = 'hex') { 196function 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
200function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
201 return createHash('sha1').update(str).digest(encoding)
202}
203
200function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { 204function 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
14export { 16export {
15 isCacheFileObjectValid 17 isCacheFileObjectValid
16} 18}
19
20// ---------------------------------------------------------------------------
21
22function 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 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
3import { peertubeTruncate } from '../../core-utils' 3import { peertubeTruncate } from '../../core-utils'
4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 4import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
5import { 5import {
6 isVideoDurationValid, 6 isVideoDurationValid,
7 isVideoNameValid, 7 isVideoNameValid,
@@ -12,7 +12,6 @@ import {
12} from '../videos' 12} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
14import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses'
16 15
17function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 16function 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
16function isArrayOf (value: any, validator: (value: any) => boolean) {
17 return isArray(value) && value.every(v => validator(v))
18}
19
16function isDateValid (value: string) { 20function 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
83export { 87export {
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
89function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 89function 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 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
29 return resolutionsEnabled 29 return resolutionsEnabled
30} 30}
31 31
32async function getVideoFileResolution (path: string) { 32async 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
41async 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
41async function getVideoFileFPS (path: string) { 50async 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
110type TranscodeOptions = { 119type 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
117function transcode (options: TranscodeOptions) { 130function 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
168export { 193export {
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
8function doRequest <T> ( 8function 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'
7import { Instance as ParseTorrent } from 'parse-torrent' 7import { Instance as ParseTorrent } from 'parse-torrent'
8import { remove } from 'fs-extra' 8import { remove } from 'fs-extra'
9import * as memoizee from 'memoizee' 9import * as memoizee from 'memoizee'
10import { isArray } from './custom-validators/misc'
11 10
12function deleteFileAsync (path: string) { 11function 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 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { 5function 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
19const LAST_MIGRATION_VERSION = 325 19const 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
642const HLS_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.PLAYLISTS_DIR, 'hls')
643const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
644
635const MEMOIZE_TTL = { 645const 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
706updateWebserverUrls() 718updateWebserverUrls()
@@ -709,6 +721,7 @@ updateWebserverUrls()
709 721
710export { 722export {
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'
33import { ServerBlocklistModel } from '../models/server/server-blocklist' 33import { ServerBlocklistModel } from '../models/server/server-blocklist'
34import { UserNotificationModel } from '../models/account/user-notification' 34import { UserNotificationModel } from '../models/account/user-notification'
35import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 35import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
36import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
36 37
37require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 38require('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'
6import { ApplicationModel } from '../models/application/application' 6import { ApplicationModel } from '../models/application/application'
7import { OAuthClientModel } from '../models/oauth/oauth-client' 7import { OAuthClientModel } from '../models/oauth/oauth-client'
8import { applicationExist, clientsExist, usersExist } from './checker-after-init' 8import { applicationExist, clientsExist, usersExist } from './checker-after-init'
9import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' 9import { CACHE, CONFIG, HLS_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants'
10import { sequelizeTypescript } from './database' 10import { sequelizeTypescript } from './database'
11import { remove, ensureDir } from 'fs-extra' 11import { 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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
44function down (options) {
45 throw new Error('Not implemented.')
46}
47
48export {
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 @@
1import { CacheFileObject } from '../../../shared/index' 1import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index'
2import { VideoModel } from '../../models/video/video' 2import { VideoModel } from '../../models/video/video'
3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
4import { Transaction } from 'sequelize' 4import { Transaction } from 'sequelize'
5import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
5 6
6function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { 7function 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
26async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { 26async 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
73async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { 73async 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
61async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { 61async 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'
5import { VideoAbuseModel } from '../../models/video/video-abuse' 5import { VideoAbuseModel } from '../../models/video/video-abuse'
6import { VideoCommentModel } from '../../models/video/video-comment' 6import { VideoCommentModel } from '../../models/video/video-comment'
7import { VideoFileModel } from '../../models/video/video-file' 7import { VideoFileModel } from '../../models/video/video-file'
8import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
9import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
8 10
9function getVideoActivityPubUrl (video: VideoModel) { 11function 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
21function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) {
22 return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}`
23}
24
19function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { 25function 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
93export { 99export {
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'
2import * as sequelize from 'sequelize' 2import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as request from 'request' 4import * as request from 'request'
5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import {
6 ActivityIconObject,
7 ActivityPlaylistSegmentHashesObject,
8 ActivityPlaylistUrlObject,
9 ActivityUrlObject,
10 ActivityVideoUrlObject,
11 VideoState
12} from '../../../shared/index'
6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 13import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
7import { VideoPrivacy } from '../../../shared/models/videos' 14import { VideoPrivacy } from '../../../shared/models/videos'
8import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 15import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -30,6 +37,9 @@ import { AccountModel } from '../../models/account/account'
30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 37import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
31import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 38import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
32import { Notifier } from '../notifier' 39import { Notifier } from '../notifier'
40import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
41import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
42import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
33 43
34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 44async 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
370function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 399function 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
406function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
407 const urlMediaType = url.mediaType || url.mimeType
408
409 return urlMediaType === 'application/x-mpegURL'
410}
411
412function 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
377async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 418async 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
475function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { 522function 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
560function 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 @@
1import { VideoModel } from '../models/video/video'
2import { basename, join, dirname } from 'path'
3import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
4import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
5import { getVideoFileSize } from '../helpers/ffmpeg-utils'
6import { sha256 } from '../helpers/core-utils'
7import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
8import { logger } from '../helpers/logger'
9import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
10import { generateRandomString } from '../helpers/utils'
11import { flatten, uniq } from 'lodash'
12
13async 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
40async 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
75function 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
91function 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
158export {
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'
5import { JobQueue } from '../job-queue' 5import { JobQueue } from '../job-queue'
6import { federateVideoIfNeeded } from '../../activitypub' 6import { federateVideoIfNeeded } from '../../activitypub'
7import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript, CONFIG } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' 11import { generateHlsPlaylist, importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 12import { Notifier } from '../../notifier'
13 13
14export type VideoFilePayload = { 14export 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
21export type VideoFileImportPayload = { 22export 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
68async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 72async 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
86async 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
100async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { 121async 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
191function 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 @@
1import { AbstractScheduler } from './abstract-scheduler' 1import { AbstractScheduler } from './abstract-scheduler'
2import { CONFIG, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' 2import { CONFIG, HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { VideosRedundancy } from '../../../shared/models/redundancy' 4import { VideosRedundancy } from '../../../shared/models/redundancy'
5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
@@ -9,9 +9,19 @@ import { join } from 'path'
9import { move } from 'fs-extra' 9import { move } from 'fs-extra'
10import { getServerActor } from '../../helpers/utils' 10import { getServerActor } from '../../helpers/utils'
11import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' 11import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
12import { getVideoCacheFileActivityPubUrl } from '../activitypub/url' 12import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
13import { removeVideoRedundancy } from '../redundancy' 13import { removeVideoRedundancy } from '../redundancy'
14import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' 14import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16import { VideoModel } from '../../models/video/video'
17import { downloadPlaylistSegments } from '../hls'
18
19type CandidateToDuplicate = {
20 redundancy: VideosRedundancy,
21 video: VideoModel,
22 files: VideoFileModel[],
23 streamingPlaylists: VideoStreamingPlaylistModel[]
24}
15 25
16export class VideosRedundancyScheduler extends AbstractScheduler { 26export 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 @@
1import { CONFIG } from '../initializers' 1import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, move, stat } from 'fs-extra' 4import { copy, ensureDir, move, remove, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { VideoResolution } from '../../shared/models/videos' 6import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { VideoFileModel } from '../models/video/video-file'
8import { VideoModel } from '../models/video/video' 8import { VideoModel } from '../models/video/video'
9import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
11import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
9 12
10async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { 13async 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
50async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { 54async 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
91async 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
87async function importVideoFile (video: VideoModel, inputFilePath: string) { 126async 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
127export { 166export {
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'
13import { SERVER_ACTOR_NAME } from '../../initializers' 13import { SERVER_ACTOR_NAME } from '../../initializers'
14import { ServerModel } from '../../models/server/server' 14import { ServerModel } from '../../models/server/server'
15 15
16const videoRedundancyGetValidator = [ 16const 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
48const 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
77export { 101export {
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
114const usersUpdateValidator = [ 114const 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'
27import { VideoImportModel } from '../video/video-import' 27import { VideoImportModel } from '../video/video-import'
28import { ActorModel } from '../activitypub/actor' 28import { ActorModel } from '../activitypub/actor'
29import { ActorFollowModel } from '../activitypub/actor-follow' 29import { ActorFollowModel } from '../activitypub/actor-follow'
30import { AvatarModel } from '../avatar/avatar'
31import { ServerModel } from '../server/server'
30 32
31enum ScopeNames { 33enum ScopeNames {
32 WITH_ALL = 'WITH_ALL' 34 WITH_ALL = 'WITH_ALL'
33} 35}
34 36
37function 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
35function buildVideoInclude (required: boolean) { 57function 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
43function buildChannelInclude (required: boolean) { 65function 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
51function buildAccountInclude (required: boolean) { 74function 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'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import * as Sequelize from 'sequelize' 30import * as Sequelize from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
31 32
32export enum ScopeNames { 33export 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 @@
1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import {
5 ActivityPlaylistInfohashesObject,
6 ActivityPlaylistSegmentHashesObject,
7 ActivityUrlObject,
8 VideoTorrentObject
9} from '../../../shared/models/activitypub/objects'
5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' 10import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 11import { VideoCaptionModel } from './video-caption'
7import { 12import {
@@ -11,6 +16,8 @@ import {
11 getVideoSharesActivityPubUrl 16 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 17} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc' 18import { isArray } from '../../helpers/custom-validators/misc'
19import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
20import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
14 21
15export type VideoFormattingJSONOptions = { 22export 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
161function 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
145function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { 180function 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 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
3import { throwIfNotValid } from '../utils'
4import { VideoModel } from './video'
5import * as Sequelize from 'sequelize'
6import { VideoRedundancyModel } from '../redundancy/video-redundancy'
7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
8import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
9import { CONSTRAINTS_FIELDS, STATIC_PATHS } from '../../initializers'
10import { VideoFileModel } from './video-file'
11import { join } from 'path'
12import { sha1 } from '../../helpers/core-utils'
13import { 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})
31export 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'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { VideoImportModel } from './video-import' 97import { VideoImportModel } from './video-import'
98import { 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
100const indexes: Sequelize.DefineIndexesOptions[] = [ 101const 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
165type ForAPIOptions = { 168type 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'
11import { getAccount } from '../../../../shared/utils/users/accounts' 11import { getAccount } from '../../../../shared/utils/users/accounts'
12 12
13describe('Test users API validators', function () { 13describe('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'
22import { waitJobs } from '../../../../shared/utils/server/jobs' 22import { waitJobs } from '../../../../shared/utils/server/jobs'
23 23
@@ -48,6 +48,11 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe
48 48
49async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { 49async 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
88async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { 93async 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
108async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { 111async 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
122async 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
136async 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
152async 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
166async 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
203async 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
217async 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
177async function enableRedundancyOnServer1 () { 231async 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'
8import './video-channels' 8import './video-channels'
9import './video-comments' 9import './video-comments'
10import './video-description' 10import './video-description'
11import './video-hls'
11import './video-imports' 12import './video-imports'
12import './video-nsfw' 13import './video-nsfw'
13import './video-privacy' 14import './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
3import * as chai from 'chai'
4import 'mocha'
5import {
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'
22import { VideoDetails } from '../../../../shared/models/videos'
23import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
24import { join } from 'path'
25
26const expect = chai.expect
27
28async 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
71describe('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 @@
1import { ActivityVideoUrlObject } from './common-objects' 1import { ActivityVideoUrlObject, ActivityPlaylistUrlObject } from './common-objects'
2 2
3export interface CacheFileObject { 3export 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
31export type ActivityUrlObject = 31export 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 40export 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) 45export 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
54export 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
63export 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
71export type ActivityUrlObject = ActivityVideoUrlObject | ActivityPlaylistUrlObject | ActivityBitTorrentUrlObject | ActivityHtmlUrlObject
50 72
51export interface ActivityPubAttributedTo { 73export 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
25export interface ActorInfo {
26 id: number
27 displayName: string
28 name: string
29 host: string
30 avatar?: {
31 path: string
32 }
33}
34
25export interface UserNotification { 35export 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 @@
1import { UserRole } from './user-role' 1import { UserRole } from './user-role'
2 2
3export interface UserUpdate { 3export 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 @@
1import { VideoStreamingPlaylistType } from './video-streaming-playlist.type'
2
3export 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 @@
1export 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'
5import { VideoPrivacy } from './video-privacy.enum' 5import { VideoPrivacy } from './video-privacy.enum'
6import { VideoScheduleUpdate } from './video-schedule-update.model' 6import { VideoScheduleUpdate } from './video-schedule-update.model'
7import { VideoConstant } from './video-constant.model' 7import { VideoConstant } from './video-constant.model'
8import { VideoStreamingPlaylist } from './video-streaming-playlist.model'
8 9
9export interface VideoFile { 10export 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
30export interface AccountAttribute { 31export 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
40export interface Video { 41export 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'
17export * from './videos/video-abuses' 17export * from './videos/video-abuses'
18export * from './videos/video-blacklist' 18export * from './videos/video-blacklist'
19export * from './videos/video-channels' 19export * from './videos/video-channels'
20export * from './videos/video-comments'
21export * from './videos/video-playlists'
20export * from './videos/videos' 22export * from './videos/videos'
21export * from './videos/video-change-ownership' 23export * from './videos/video-change-ownership'
22export * from './feeds/feeds' 24export * 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 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { buildAbsoluteFixturePath, root } from '../miscs/miscs' 2import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
3import { isAbsolute, join } from 'path' 3import { isAbsolute, join } from 'path'
4import { parse } from 'url'
5
6function 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
5function makeGetRequest (options: { 12function 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
164async function checkTmpIsEmpty (server: ServerInfo) { 168async function checkTmpIsEmpty (server: ServerInfo) {
169 return checkDirectoryIsEmpty(server, 'tmp')
170}
171
172async 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
197export { 205export {
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) {
146function checkActor (actor: any) { 146function 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
151function checkComment (comment: any, commentId: number, threadId: number) { 152function 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 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5
6function getPlaylist (url: string, statusCodeExpected = 200) {
7 return makeRawRequest(url, statusCodeExpected)
8}
9
10function getSegment (url: string, statusCodeExpected = 200, range?: string) {
11 return makeRawRequest(url, statusCodeExpected, range)
12}
13
14function getSegmentSha256 (url: string, statusCodeExpected = 200) {
15 return makeRawRequest(url, statusCodeExpected)
16}
17
18async 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
46export {
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
271async function checkVideoFilesWereRemoved ( 271async 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 @@
1openapi: 3.0.0 1openapi: 3.0.0
2info: 2info:
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}