Browse Source

rework video player

Klaudiusz Dembler 4 years ago
parent
commit
57dad754b8
32 changed files with 845 additions and 220 deletions
  1. 6 0
      .eslintrc.js
  2. 5 1
      packages/app/package.json
  3. 3 1
      packages/app/src/App.tsx
  4. 9 85
      packages/app/src/components/VideoGallery.tsx
  5. 83 0
      packages/app/src/config/mockData.ts
  6. 3 0
      packages/app/src/config/routes.ts
  7. 1 1
      packages/app/src/shared/.storybook/theme.js
  8. 3 1
      packages/app/src/shared/components/Carousel/Carousel.style.ts
  9. 23 0
      packages/app/src/shared/components/ChannelAvatar/ChannelAvatar.style.ts
  10. 19 0
      packages/app/src/shared/components/ChannelAvatar/ChannelAvatar.tsx
  11. 3 0
      packages/app/src/shared/components/ChannelAvatar/index.ts
  12. 1 2
      packages/app/src/shared/components/Grid/Grid.style.ts
  13. 138 21
      packages/app/src/shared/components/VideoPlayer/VideoPlayer.style.tsx
  14. 66 68
      packages/app/src/shared/components/VideoPlayer/VideoPlayer.tsx
  15. 76 0
      packages/app/src/shared/components/VideoPlayer/videoJsPlayer.ts
  16. 4 3
      packages/app/src/shared/components/VideoPreview/VideoPreview.styles.tsx
  17. 3 1
      packages/app/src/shared/components/VideoPreview/VideoPreview.tsx
  18. 1 0
      packages/app/src/shared/components/index.ts
  19. 1 1
      packages/app/src/shared/icons/index.ts
  20. 1 1
      packages/app/src/shared/icons/play.svg
  21. 0 27
      packages/app/src/shared/stories/13-VideoPreview.stories.tsx
  22. 28 0
      packages/app/src/shared/stories/15-VideoPlayer.stories.tsx
  23. 27 0
      packages/app/src/shared/stories/16-VideoPreview.stories.tsx
  24. 6 0
      packages/app/src/types/channel.ts
  25. 12 0
      packages/app/src/types/video.ts
  26. 5 0
      packages/app/src/utils/date.ts
  27. 3 0
      packages/app/src/utils/number.ts
  28. 69 0
      packages/app/src/views/VideoView/VideoView.style.tsx
  29. 75 0
      packages/app/src/views/VideoView/VideoView.tsx
  30. 1 0
      packages/app/src/views/VideoView/index.ts
  31. 2 1
      packages/app/src/views/index.ts
  32. 168 6
      yarn.lock

+ 6 - 0
.eslintrc.js

@@ -10,5 +10,11 @@ module.exports = {
     'react/prop-types': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/no-empty-function': 'warn',
+    '@typescript-eslint/ban-ts-comment': [
+      'error',
+      {
+        'ts-ignore': 'allow-with-description',
+      },
+    ],
   },
 }

+ 5 - 1
packages/app/package.json

@@ -41,14 +41,17 @@
     "@storybook/preset-create-react-app": "^3.1.4",
     "@storybook/react": "^5.3.17",
     "@storybook/theming": "^5.3.19",
+    "@types/luxon": "^1.24.1",
     "@types/reach__router": "^1.3.5",
     "@types/react": "^16.9.0",
     "@types/react-dom": "^16.9.0",
     "@types/react-redux": "^7.1.9",
     "@types/redux": "^3.6.0",
+    "@types/video.js": "^7.3.10",
     "chromatic": "^4.0.3",
     "customize-cra": "^1.0.0",
     "emotion-normalize": "^10.1.0",
+    "luxon": "^1.24.1",
     "react": "^16.13.1",
     "react-app-rewired": "^2.1.6",
     "react-docgen-typescript-loader": "^3.7.1",
@@ -59,7 +62,8 @@
     "react-spring": "^8.0.27",
     "redux": "^4.0.5",
     "storybook-addon-jsx": "^7.1.15",
-    "use-resize-observer": "^6.1.0"
+    "use-resize-observer": "^6.1.0",
+    "video.js": "^7.8.3"
   },
   "browserslist": {
     "production": [

+ 3 - 1
packages/app/src/App.tsx

@@ -4,7 +4,8 @@ import { Router } from '@reach/router'
 
 import store from './store'
 import { Layout } from './components'
-import { HomeView } from './views'
+import { HomeView, VideoView } from './views'
+import routes from './config/routes'
 
 export default function App() {
   return (
@@ -12,6 +13,7 @@ export default function App() {
       <Layout>
         <Router primary={false}>
           <HomeView default />
+          <VideoView path={routes.video} />
         </Router>
       </Layout>
     </Provider>

+ 9 - 85
packages/app/src/components/VideoGallery.tsx

@@ -1,104 +1,22 @@
 import React, { useCallback, useState } from 'react'
 import { css, SerializedStyles } from '@emotion/core'
+import { navigate } from '@reach/router'
 
 import { Gallery, VideoPreview } from '@/shared/components'
+import { MOCK_VIDEOS } from '@/config/mockData'
 
 type VideoGalleryProps = {
   title: string
   action: string
 }
 
-const videoPlaceholders = [
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/57d7/47bd/e40e51d45107656c92b3c9d982e76c6e?Expires=1593993600&Signature=Q34ageNdR3Y48K5lG8HW1KOgMGCi94qmWOK5yO~cK6XzgyO2nCJB6Pjoaa~gQX7zMVSHIkxlVt-9CHz~H9iXmA7r0LfTm90sNlTZ8ZspvU9TWgCGdPMj1A-SzTIAmeiDrZ0DrSzILQdJOMwaP-DeYKpFN6zvG6h56XHznX5lEiawRqeObL0g4SAHNG5tiO0Rdjwtckeuz~diwLVBOUqPaeDGOABlpUJQFoy~Dx7FxK65MyPXUZ7CtUVqjKkyl-jfnl0DwIpOiI9K2HJjtV7FlPbc28C4JeniafBr0nUkoWAjzcyyOr37v0xNw2EBy91mBc33kDUlwbqom9f-leRPZQ__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/bd94/067c/4750be7f93ae65cf6828daa25ae301f0?Expires=1593388800&Signature=ItXPW-xlNDLNziv7Maamv-n6HRo4UNRnfaQGBGUhHQUKZYplg2RtRlf198c-U5rDBU0Oe9UBbC8JUAMZRs1jnKs1I~UTBS0Dpq-qn1wR-q0ZJZRNmUHmiOjQ9j9GekidMJHEq-rh7qZB38ttdI72Db46WOeuA6NxvRYqe8j1CUXKpgNL-nfLcvofSYQWpTZ6n1aA4IpzlzT2ginbOhesp2yJHkgBbkIQm9It~SnTl7EoVzzDErD5KjmltO4nUBWTKYmgEdf0zEaNPQn8fF2vtKUQtz9TkdwyeVjHqhSd9aptH2vdKSZhbm8KO15BIs1ZczuQ05YQJ2RJfU8fbu5NUg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    createdAt: '2 weeks ago',
-    views: '345k',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/8e21/18b4/c0d8f1114b47062b35c47fb568fb48a5?Expires=1593388800&Signature=ZaBi7Wf9-bGZbtyqlbCl-TNCZDiWaaoiO3tMyYJVliHhuCvafyheMpBUPn8XSZQsntvmSW2A1rfLfPw-e-t1tCUw6uBIOWexRRhoNDfHWvXLUy4bcij2-D54Mo7LyL1ASLLzoTTDVCLoESNFEb3X8ntBWNOOVIy-1qLYuBdeP1vdJmcov5I4U46ge0sSPvetv0SS~3wW4CYO7zZ8Wi1CrA8mxQiMsnXCl97Vat5ngT46QZzZwVOtcmXH8KxQcn-e0MWBZFMAB5vzs2SdAX~l~3z7McrULM9-wPavJiw7dH4fUbVH7qQ-oS9dZ0t2sod3dRPGTj73Su3WVL2RlnQUqg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/f712/25ac/62a18f581f6a5aae66ac4e9b7b21fafc?Expires=1593388800&Signature=YO2e7eBwPVF05C4tlUeWVmq5o3KOpo9yyoM1khSbywLj1Kv1BIIFdjD6IBLO9Oifm8A-Ws2IlypjWuykVe9nydcoCp28VzQ~ifYtfr97plLxVcOYiYMu7HDIEpPK4UKZi3AFSvqdIAUrr-YBu2noRt8Kg-BbzMG2DV3zKTAga2zMJ-cYu5-xiW6gctEr8nl6QF7luvBYZkHTYRaMTXpsRdN-b2VoW7BX1mxm1FDWyhZ9aaEqZ8dIE3ct8t6J2ybxryrMVxLpjP-T0124N27Z9Xp4WzSTOSjfBjn~Q7mtqHk141~pq1-dQe1dO2V7JQN3AvTt4eEeCvpXQqbA0VMPnw__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/4baa/8670/e77491e871584e97b955d728627020f3?Expires=1593388800&Signature=LkSz8F8wftV81dxfumpYE5CToghiq5a3hvwywrhwVAXFwQvNzfymCImBCY9wIxILX4nnOlaNX0g~~iZ2RXD104VvLn6q967Hlg0uAH1h3eutIZzb4sQ0i4R-wXUw5ZNy2QjLCOIlKuCXMXSO1UiI8t8CJtdel9XaSqm1u~JsPFhRjRlO60RCi2cM2pJ~8G~kofCMT6J3NMgyRruVLUDY9FPGh5dI5XRFmeKbMThO2IT5KfUGsPDLb98i8gBrkLe2EKMx2DxHlDxcA3j2LVtf4yaGG-EmeDrQflknjdoAY0BKfC8Mob10ldGsYnJsMHOraz971POWOVpWTuoF0h6nNg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/4baa/8670/e77491e871584e97b955d728627020f3?Expires=1593388800&Signature=LkSz8F8wftV81dxfumpYE5CToghiq5a3hvwywrhwVAXFwQvNzfymCImBCY9wIxILX4nnOlaNX0g~~iZ2RXD104VvLn6q967Hlg0uAH1h3eutIZzb4sQ0i4R-wXUw5ZNy2QjLCOIlKuCXMXSO1UiI8t8CJtdel9XaSqm1u~JsPFhRjRlO60RCi2cM2pJ~8G~kofCMT6J3NMgyRruVLUDY9FPGh5dI5XRFmeKbMThO2IT5KfUGsPDLb98i8gBrkLe2EKMx2DxHlDxcA3j2LVtf4yaGG-EmeDrQflknjdoAY0BKfC8Mob10ldGsYnJsMHOraz971POWOVpWTuoF0h6nNg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/e0d6/4163/147cd1502ba0a99ea7258f31be9812b3?Expires=1593388800&Signature=av5e4oaaYXVDxA9U0DAQKuwA-vyC4VRaGYz2lO5IIaz-CP55B717sXn-V6H6JzMNZ7JavA9q8~uMK-r9jd-yxoia1UopEiUeSkZgQlbBhd4DEVBPoMhMF3nC--mZ4bQnVvK9xN933TCm9z46jkAOZ7U8UYp5TlA829Rnmgw3QkaNlPLW5UkgU04k6pAae5Y3t9pAnwErmDCDjHuaXKrCjCuG0zwk84f0JQAcZt4VojD8kVDFCf0IOHdZtIQMUk47CQfdWJ2KDBkm1oOcVAweTSHXdwzpiFQ~o1i-hZVYtgzIiGTE6IqW3qsksqiLtWwMsui2dvWXVu2actRRwR~OTg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/63ab/b514/2a10cf17ceaf00972e7c12c18cacd832?Expires=1593388800&Signature=WRk44Vxkc1mYbXSdGroHP8Hvd4dfhtWMukS~2yfNlY6KSb94ZzeFaKID8dMj4wznZo-sSp~lAagV7NJMFO9QLCEw0E7YeX3TnOWeFDrx63wkc388h-srSxxrNVUpB6WUCfkPtdusDrfKOx2HvM6zZLlSgYs4-McX2VTJ-g7bKx2Pzt64mox6wTGlbXWREkV2sbLSBSF6P0fpwge3VPt5jX1E~vDJ8X4mtZgN1DRIQMtc2dfZhZhyL1jNeIZe5WJyOcY9b2E3KL54pWt13UGsI08nTzloqOPFAt~fHwbwUbwWeelbjkzSABh55KYVNePerISUIy~esGJ11px7l9oWTw__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-  {
-    title: 'Sample Video Title',
-    channel: 'Channel Name',
-    showChannel: true,
-    views: '345k',
-    createdAt: '2 weeks ago',
-    poster:
-      'https://s3-alpha-sig.figma.com/img/b327/39be/143853e8ca345927ee54a7ca9acaed0c?Expires=1593388800&Signature=G1vikiLUBwdBdrzS3RC1WScSJSAAD7FEteNX4bfPf0VQL0w9fsMfha31S-l6EHxU~WTh3GAic8X5furwHjwLbE2nVUIiA0q6Rizxfd8kT97Z-9uQzPbCkla6a1qIX-7wq19t2evAVST--Sz7wfEYVCUTdZaEmqgBsJnYI3fl7nzrwLhaaTTN3liOTltgvlQn8btRClliTWENyHBwu6cwvMHuX6ePt5swhIj4gz34F0MyY~PtnsFbBHFerShd3t2DYsMqDv8CEB089v6IYZh87Rnqk7jJFZC2ZBULhjlwKQpboJOI35cov3aGOnuTe5vcmULLSfEXuV5kWJrtzJnQlA__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
-  },
-]
-
 const articleStyles = css`
   max-width: 320px;
   margin-right: 1.25rem;
 `
 
 const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) => {
-  const videos = videoPlaceholders.concat(videoPlaceholders).concat(videoPlaceholders)
+  const videos = [...MOCK_VIDEOS, ...MOCK_VIDEOS, ...MOCK_VIDEOS]
   const [controlsTop, setControlsTop] = useState<SerializedStyles>(css``)
 
   const CAROUSEL_WHEEL_HEIGHT = 48
@@ -109,6 +27,11 @@ const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) =
       `)
     }
   }, [])
+
+  const handleVideoClick = () => {
+    navigate('/video/fake')
+  }
+
   return (
     <Gallery title={title} action={action} leftControlCss={controlsTop} rightControlCss={controlsTop}>
       {videos.map((video, idx) => (
@@ -122,6 +45,7 @@ const VideoGallery: React.FC<Partial<VideoGalleryProps>> = ({ title, action }) =
             imgRef={idx === 0 ? imgRef : null}
             poster={video.poster}
             showMeta={true}
+            onClick={handleVideoClick}
           />
         </article>
       ))}

+ 83 - 0
packages/app/src/config/mockData.ts

@@ -0,0 +1,83 @@
+export const MOCK_VIDEOS = [
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/57d7/47bd/e40e51d45107656c92b3c9d982e76c6e?Expires=1593993600&Signature=Q34ageNdR3Y48K5lG8HW1KOgMGCi94qmWOK5yO~cK6XzgyO2nCJB6Pjoaa~gQX7zMVSHIkxlVt-9CHz~H9iXmA7r0LfTm90sNlTZ8ZspvU9TWgCGdPMj1A-SzTIAmeiDrZ0DrSzILQdJOMwaP-DeYKpFN6zvG6h56XHznX5lEiawRqeObL0g4SAHNG5tiO0Rdjwtckeuz~diwLVBOUqPaeDGOABlpUJQFoy~Dx7FxK65MyPXUZ7CtUVqjKkyl-jfnl0DwIpOiI9K2HJjtV7FlPbc28C4JeniafBr0nUkoWAjzcyyOr37v0xNw2EBy91mBc33kDUlwbqom9f-leRPZQ__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/bd94/067c/4750be7f93ae65cf6828daa25ae301f0?Expires=1593388800&Signature=ItXPW-xlNDLNziv7Maamv-n6HRo4UNRnfaQGBGUhHQUKZYplg2RtRlf198c-U5rDBU0Oe9UBbC8JUAMZRs1jnKs1I~UTBS0Dpq-qn1wR-q0ZJZRNmUHmiOjQ9j9GekidMJHEq-rh7qZB38ttdI72Db46WOeuA6NxvRYqe8j1CUXKpgNL-nfLcvofSYQWpTZ6n1aA4IpzlzT2ginbOhesp2yJHkgBbkIQm9It~SnTl7EoVzzDErD5KjmltO4nUBWTKYmgEdf0zEaNPQn8fF2vtKUQtz9TkdwyeVjHqhSd9aptH2vdKSZhbm8KO15BIs1ZczuQ05YQJ2RJfU8fbu5NUg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    createdAt: '2 weeks ago',
+    views: '345k',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/8e21/18b4/c0d8f1114b47062b35c47fb568fb48a5?Expires=1593388800&Signature=ZaBi7Wf9-bGZbtyqlbCl-TNCZDiWaaoiO3tMyYJVliHhuCvafyheMpBUPn8XSZQsntvmSW2A1rfLfPw-e-t1tCUw6uBIOWexRRhoNDfHWvXLUy4bcij2-D54Mo7LyL1ASLLzoTTDVCLoESNFEb3X8ntBWNOOVIy-1qLYuBdeP1vdJmcov5I4U46ge0sSPvetv0SS~3wW4CYO7zZ8Wi1CrA8mxQiMsnXCl97Vat5ngT46QZzZwVOtcmXH8KxQcn-e0MWBZFMAB5vzs2SdAX~l~3z7McrULM9-wPavJiw7dH4fUbVH7qQ-oS9dZ0t2sod3dRPGTj73Su3WVL2RlnQUqg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/f712/25ac/62a18f581f6a5aae66ac4e9b7b21fafc?Expires=1593388800&Signature=YO2e7eBwPVF05C4tlUeWVmq5o3KOpo9yyoM1khSbywLj1Kv1BIIFdjD6IBLO9Oifm8A-Ws2IlypjWuykVe9nydcoCp28VzQ~ifYtfr97plLxVcOYiYMu7HDIEpPK4UKZi3AFSvqdIAUrr-YBu2noRt8Kg-BbzMG2DV3zKTAga2zMJ-cYu5-xiW6gctEr8nl6QF7luvBYZkHTYRaMTXpsRdN-b2VoW7BX1mxm1FDWyhZ9aaEqZ8dIE3ct8t6J2ybxryrMVxLpjP-T0124N27Z9Xp4WzSTOSjfBjn~Q7mtqHk141~pq1-dQe1dO2V7JQN3AvTt4eEeCvpXQqbA0VMPnw__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/4baa/8670/e77491e871584e97b955d728627020f3?Expires=1593388800&Signature=LkSz8F8wftV81dxfumpYE5CToghiq5a3hvwywrhwVAXFwQvNzfymCImBCY9wIxILX4nnOlaNX0g~~iZ2RXD104VvLn6q967Hlg0uAH1h3eutIZzb4sQ0i4R-wXUw5ZNy2QjLCOIlKuCXMXSO1UiI8t8CJtdel9XaSqm1u~JsPFhRjRlO60RCi2cM2pJ~8G~kofCMT6J3NMgyRruVLUDY9FPGh5dI5XRFmeKbMThO2IT5KfUGsPDLb98i8gBrkLe2EKMx2DxHlDxcA3j2LVtf4yaGG-EmeDrQflknjdoAY0BKfC8Mob10ldGsYnJsMHOraz971POWOVpWTuoF0h6nNg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/4baa/8670/e77491e871584e97b955d728627020f3?Expires=1593388800&Signature=LkSz8F8wftV81dxfumpYE5CToghiq5a3hvwywrhwVAXFwQvNzfymCImBCY9wIxILX4nnOlaNX0g~~iZ2RXD104VvLn6q967Hlg0uAH1h3eutIZzb4sQ0i4R-wXUw5ZNy2QjLCOIlKuCXMXSO1UiI8t8CJtdel9XaSqm1u~JsPFhRjRlO60RCi2cM2pJ~8G~kofCMT6J3NMgyRruVLUDY9FPGh5dI5XRFmeKbMThO2IT5KfUGsPDLb98i8gBrkLe2EKMx2DxHlDxcA3j2LVtf4yaGG-EmeDrQflknjdoAY0BKfC8Mob10ldGsYnJsMHOraz971POWOVpWTuoF0h6nNg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/e0d6/4163/147cd1502ba0a99ea7258f31be9812b3?Expires=1593388800&Signature=av5e4oaaYXVDxA9U0DAQKuwA-vyC4VRaGYz2lO5IIaz-CP55B717sXn-V6H6JzMNZ7JavA9q8~uMK-r9jd-yxoia1UopEiUeSkZgQlbBhd4DEVBPoMhMF3nC--mZ4bQnVvK9xN933TCm9z46jkAOZ7U8UYp5TlA829Rnmgw3QkaNlPLW5UkgU04k6pAae5Y3t9pAnwErmDCDjHuaXKrCjCuG0zwk84f0JQAcZt4VojD8kVDFCf0IOHdZtIQMUk47CQfdWJ2KDBkm1oOcVAweTSHXdwzpiFQ~o1i-hZVYtgzIiGTE6IqW3qsksqiLtWwMsui2dvWXVu2actRRwR~OTg__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/63ab/b514/2a10cf17ceaf00972e7c12c18cacd832?Expires=1593388800&Signature=WRk44Vxkc1mYbXSdGroHP8Hvd4dfhtWMukS~2yfNlY6KSb94ZzeFaKID8dMj4wznZo-sSp~lAagV7NJMFO9QLCEw0E7YeX3TnOWeFDrx63wkc388h-srSxxrNVUpB6WUCfkPtdusDrfKOx2HvM6zZLlSgYs4-McX2VTJ-g7bKx2Pzt64mox6wTGlbXWREkV2sbLSBSF6P0fpwge3VPt5jX1E~vDJ8X4mtZgN1DRIQMtc2dfZhZhyL1jNeIZe5WJyOcY9b2E3KL54pWt13UGsI08nTzloqOPFAt~fHwbwUbwWeelbjkzSABh55KYVNePerISUIy~esGJ11px7l9oWTw__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+  {
+    title: 'Sample Video Title',
+    channel: 'Channel Name',
+    showChannel: true,
+    views: '345k',
+    createdAt: '2 weeks ago',
+    poster:
+      'https://s3-alpha-sig.figma.com/img/b327/39be/143853e8ca345927ee54a7ca9acaed0c?Expires=1593388800&Signature=G1vikiLUBwdBdrzS3RC1WScSJSAAD7FEteNX4bfPf0VQL0w9fsMfha31S-l6EHxU~WTh3GAic8X5furwHjwLbE2nVUIiA0q6Rizxfd8kT97Z-9uQzPbCkla6a1qIX-7wq19t2evAVST--Sz7wfEYVCUTdZaEmqgBsJnYI3fl7nzrwLhaaTTN3liOTltgvlQn8btRClliTWENyHBwu6cwvMHuX6ePt5swhIj4gz34F0MyY~PtnsFbBHFerShd3t2DYsMqDv8CEB089v6IYZh87Rnqk7jJFZC2ZBULhjlwKQpboJOI35cov3aGOnuTe5vcmULLSfEXuV5kWJrtzJnQlA__&Key-Pair-Id=APKAINTVSUGEWH5XD5UA',
+  },
+]

+ 3 - 0
packages/app/src/config/routes.ts

@@ -0,0 +1,3 @@
+export default {
+  video: 'video/:id',
+}

+ 1 - 1
packages/app/src/shared/.storybook/theme.js

@@ -28,7 +28,7 @@ export default create({
   barSelectedColor: 'white',
 
   // Form colors
-  inputBg: 'white',
+	inputBg: 'white',
   inputBorder: '#272D33',
   inputTextColor: 'black',
   inputBorderRadius: 4,

+ 3 - 1
packages/app/src/shared/components/Carousel/Carousel.style.ts

@@ -1,4 +1,4 @@
-import { StyleFn, makeStyles } from '../../utils'
+import { makeStyles, StyleFn } from '../../utils'
 import { spacing } from '../../theme'
 
 export type CarouselStyleProps = Record<string, unknown>
@@ -9,6 +9,8 @@ const container: StyleFn = () => ({
 })
 const outerItemsContainer: StyleFn = () => ({
   overflow: 'hidden',
+  padding: `${spacing.xs} 0 0 ${spacing.xs}`,
+  margin: `-${spacing.xs} 0 0 -${spacing.xs}`,
 })
 
 const innerItemsContainer: StyleFn = () => ({

+ 23 - 0
packages/app/src/shared/components/ChannelAvatar/ChannelAvatar.style.ts

@@ -0,0 +1,23 @@
+import styled from '@emotion/styled'
+import { colors, spacing, typography } from '@/shared/theme'
+import { Avatar } from '@/shared/components'
+
+export const Container = styled.div`
+  display: flex;
+  align-items: center;
+`
+
+export const StyledAvatar = styled(Avatar)`
+  width: 32px;
+  height: 32px;
+  margin-right: ${spacing.xs};
+`
+
+export const Name = styled.span`
+  display: inline-block;
+  font-family: ${typography.fonts.headers};
+  font-size: 1rem;
+  line-height: 1;
+  font-weight: bold;
+  color: ${colors.white};
+`

+ 19 - 0
packages/app/src/shared/components/ChannelAvatar/ChannelAvatar.tsx

@@ -0,0 +1,19 @@
+import React from 'react'
+import { Container, Name, StyledAvatar } from './ChannelAvatar.style'
+
+type ChannelAvatarProps = {
+  name: string
+  avatarUrl?: string
+  className?: string
+}
+
+const ChannelAvatar: React.FC<ChannelAvatarProps> = ({ name, avatarUrl, className }) => {
+  return (
+    <Container className={className}>
+      <StyledAvatar name={name} img={avatarUrl} />
+      <Name>{name}</Name>
+    </Container>
+  )
+}
+
+export default ChannelAvatar

+ 3 - 0
packages/app/src/shared/components/ChannelAvatar/index.ts

@@ -0,0 +1,3 @@
+import ChannelAvatar from './ChannelAvatar'
+
+export default ChannelAvatar

+ 1 - 2
packages/app/src/shared/components/Grid/Grid.style.ts

@@ -1,4 +1,4 @@
-import { StyleFn, makeStyles } from '../../utils'
+import { makeStyles, StyleFn } from '../../utils'
 
 export type GridStyleProps = {
   minItemWidth?: string | number
@@ -11,7 +11,6 @@ const container: StyleFn = (_, { minItemWidth = '300', maxItemWidth }) => ({
 })
 
 const item: StyleFn = () => ({
-  cursor: 'pointer',
   width: '100%',
 })
 

+ 138 - 21
packages/app/src/shared/components/VideoPlayer/VideoPlayer.style.tsx

@@ -1,23 +1,140 @@
-import { css } from '@emotion/core'
-import { breakpoints } from '../../theme'
-
-export type VideoPlayerStyleProps = {
-  width?: string | number
-  height?: string | number
-  responsive?: boolean
-  ratio?: string
-}
-
-export const makeStyles = ({ width = '100%', height = '100%' }: VideoPlayerStyleProps) => {
-  return {
-    containerStyles: css`
-      max-width: ${breakpoints.medium};
-      & .video-player {
+import styled from '@emotion/styled'
+import { colors, spacing, typography } from '../../theme'
+import { PlayIcon } from '../../icons'
+
+export const Container = styled.div`
+  position: relative;
+
+  *:focus {
+    outline: none;
+  }
+
+  .vjs-control-bar {
+    font-family: ${typography.fonts.base};
+    background-color: rgba(0, 0, 0, 0.3);
+    height: ${spacing.xxxxxl} !important;
+    align-items: center;
+
+    /* account for progress bar on top */
+    padding: 5px ${spacing.xxl} 0;
+
+    .vjs-control {
+      height: 30px;
+
+      .vjs-icon-placeholder ::before {
+        line-height: 1.25;
+        font-size: ${typography.sizes.icon.xlarge};
       }
-    `,
-    playerStyles: css`
-      width: ${width};
-      height: ${height};
-    `,
+    }
+
+    .vjs-time-control {
+      display: inline-block;
+      font-size: ${typography.sizes.caption};
+      user-select: none;
+      height: unset;
+    }
+    .vjs-play-control {
+      order: -5;
+    }
+    .vjs-current-time {
+      order: -4;
+      padding-right: 0;
+    }
+    .vjs-time-divider {
+      order: -3;
+      padding: 0 4px;
+      min-width: 0;
+    }
+    .vjs-duration {
+      order: -2;
+      padding-left: 0;
+    }
+    .vjs-volume-panel {
+      order: -1;
+    }
+    .vjs-remaining-time {
+      display: none;
+    }
+
+    .vjs-picture-in-picture-control {
+      margin-left: auto;
+    }
+
+    .vjs-slider {
+      background-color: ${colors.gray[400]};
+
+      .vjs-slider-bar,
+      .vjs-volume-level {
+        background-color: ${colors.blue[500]};
+      }
+    }
+
+    .vjs-progress-control {
+      position: absolute;
+      top: 0;
+      left: ${spacing.xxl};
+      width: calc(100% - 2 * ${spacing.xxl});
+      height: 5px;
+
+      .vjs-progress-holder {
+        height: 100%;
+        margin: 0;
+
+        .vjs-play-progress ::before {
+          display: none;
+        }
+
+        .vjs-load-progress {
+          background-color: ${colors.gray[200]};
+
+          div {
+            background: none;
+          }
+        }
+      }
+    }
+
+    .vjs-volume-control {
+      width: 72px !important;
+      .vjs-volume-bar {
+        width: 72px;
+        margin-left: 0;
+        margin-right: 0;
+        height: 4px;
+        .vjs-volume-level {
+          height: 4px;
+          ::before {
+            font-size: ${typography.sizes.icon.small};
+            top: -0.25em;
+          }
+        }
+      }
+    }
   }
-}
+
+  .vjs-big-play-button {
+    display: none !important;
+  }
+`
+
+export const PlayOverlay = styled.div`
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 100;
+
+  background: linear-gradient(0deg, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6));
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  cursor: pointer;
+`
+
+export const StyledPlayIcon = styled(PlayIcon)`
+  height: 72px;
+  width: 72px;
+`

+ 66 - 68
packages/app/src/shared/components/VideoPlayer/VideoPlayer.tsx

@@ -1,74 +1,72 @@
-import React from 'react'
-import ReactPlayer from 'react-player'
-import { VideoPlayerStyleProps, makeStyles } from './VideoPlayer.style'
+import React, { useEffect, useState } from 'react'
+import { Container, PlayOverlay, StyledPlayIcon } from './VideoPlayer.style'
+import { useVideoJsPlayer, VideoJsConfig } from './videoJsPlayer'
 
-export type VideoPlayerProps = {
-  src?: string
-  playing?: boolean
-  poster?: string
-  controls?: boolean
-  volume?: number
-  loop?: boolean
-  autoPlay?: boolean
-  muted?: boolean
+type VideoPlayerProps = {
   className?: string
-  onReady?(): void
-  onStart?(): void
-  onPlay?(): void
-  onPause?(): void
-  onBuffer?(): void
-  onEnded?(): void
-  onError?(error: any): void
-  onDuration?(duration: number): void
-  onProgress?(state: { played: number; loaded: number }): void
-} & VideoPlayerStyleProps
+  autoplay?: boolean
+} & VideoJsConfig
+
+const VideoPlayer: React.FC<VideoPlayerProps> = ({ className, autoplay, ...videoJsConfig }) => {
+  const [player, playerRef] = useVideoJsPlayer(videoJsConfig)
+  const [playOverlayVisible, setPlayOverlayVisible] = useState(true)
+
+  useEffect(() => {
+    if (!player || !autoplay) {
+      return
+    }
+
+    const handler = async () => {
+      try {
+        await player.play()
+      } catch (e) {
+        console.warn('Autoplay failed:', e)
+      }
+    }
+
+    player.on('loadstart', handler)
+
+    return () => {
+      player.off('loadstart', handler)
+    }
+  }, [player, autoplay])
+
+  useEffect(() => {
+    if (!player) {
+      return
+    }
+
+    const handler = () => {
+      setPlayOverlayVisible(false)
+    }
+
+    player.on('play', handler)
+
+    return () => {
+      player.off('play', handler)
+    }
+  })
+
+  const handlePlayOverlayClick = () => {
+    if (!player) {
+      return
+    }
+
+    player.play()
+  }
 
-export default function VideoPlayer({
-  src,
-  poster,
-  playing,
-  autoPlay,
-  loop = false,
-  onStart,
-  onReady,
-  onPlay,
-  onBuffer,
-  onEnded,
-  onDuration,
-  onProgress,
-  className,
-  controls = true,
-  ...styleProps
-}: VideoPlayerProps) {
-  const { playerStyles, containerStyles } = makeStyles(styleProps)
   return (
-    <div css={containerStyles}>
-      <ReactPlayer
-        css={playerStyles}
-        width={styleProps.responsive ? '100%' : styleProps.width}
-        height={styleProps.responsive ? '100%' : styleProps.height}
-        url={src}
-        autoPlay={autoPlay}
-        light={poster || true}
-        className={className}
-        playing={playing}
-        loop={loop}
-        controls={controls}
-        onStart={onStart}
-        onPlay={onPlay}
-        onBuffer={onBuffer}
-        onReady={onReady}
-        onEnded={onEnded}
-        onDuration={onDuration}
-        onProgress={onProgress}
-        config={{
-          file: {
-            attributes: {
-              className: 'video-player',
-            },
-          },
-        }}
-      />
-    </div>
+    <Container className={className}>
+      {playOverlayVisible && (
+        <PlayOverlay onClick={handlePlayOverlayClick}>
+          <StyledPlayIcon />
+        </PlayOverlay>
+      )}
+      <div data-vjs-player>
+        <video ref={playerRef} className="video-js" />
+      </div>
+    </Container>
   )
 }
+
+export default VideoPlayer

+ 76 - 0
packages/app/src/shared/components/VideoPlayer/videoJsPlayer.ts

@@ -0,0 +1,76 @@
+import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
+import { RefObject, useEffect, useRef, useState } from 'react'
+import 'video.js/dist/video-js.css'
+
+export type VideoJsConfig = {
+  src: string
+  width?: number
+  height?: number
+  fluid?: boolean
+  fill?: boolean
+}
+
+type VideoJsPlayerHook = (config: VideoJsConfig) => [VideoJsPlayer | null, RefObject<HTMLVideoElement>]
+export const useVideoJsPlayer: VideoJsPlayerHook = ({ fill, fluid, height, src, width }) => {
+  const playerRef = useRef<HTMLVideoElement>(null)
+  const [player, setPlayer] = useState<VideoJsPlayer | null>(null)
+
+  useEffect(() => {
+    const videoJsOptions: VideoJsPlayerOptions = {
+      controls: true,
+    }
+
+    const playerInstance = videojs(playerRef.current, videoJsOptions)
+    setPlayer(playerInstance)
+
+    return () => {
+      playerInstance.dispose()
+    }
+  }, [])
+
+  useEffect(() => {
+    if (!player) {
+      return
+    }
+
+    player.src({
+      src,
+      type: 'video/mp4',
+    })
+  }, [player, src])
+
+  useEffect(() => {
+    if (!player || !width) {
+      return
+    }
+
+    player.width(width)
+  }, [player, width])
+
+  useEffect(() => {
+    if (!player || !height) {
+      return
+    }
+
+    player.height(height)
+  }, [player, height])
+
+  useEffect(() => {
+    if (!player) {
+      return
+    }
+
+    player.fluid(Boolean(fluid))
+  }, [player, fluid])
+
+  useEffect(() => {
+    if (!player) {
+      return
+    }
+
+    // @ts-ignore @types/video.js is outdated and doesn't provide types for some newer video.js features
+    player.fill(Boolean(fill))
+  }, [player, fill])
+
+  return [player, playerRef]
+}

+ 4 - 3
packages/app/src/shared/components/VideoPreview/VideoPreview.styles.tsx

@@ -1,7 +1,7 @@
 import styled from '@emotion/styled'
 import { colors, spacing, typography } from '../../theme'
 import Avatar from '../Avatar'
-import { Play } from '../../icons'
+import { PlayIcon } from '../../icons'
 
 const HOVER_BORDER_SIZE = '2px'
 
@@ -55,10 +55,11 @@ export const CoverHoverOverlay = styled.div`
   align-items: center;
 `
 
-// Play icon is incorrectly typed as string
-export const CoverPlayIcon = styled(Play as any)`
+export const CoverPlayIcon = styled(PlayIcon)`
   transition: transform 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
   transform: translateY(40px);
+  width: 54px;
+  height: 54px;
 `
 
 export const ProgressOverlay = styled.div`

+ 3 - 1
packages/app/src/shared/components/VideoPreview/VideoPreview.tsx

@@ -31,6 +31,7 @@ type VideoPreviewProps = {
   imgRef: React.Ref<HTMLImageElement>
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
   onChannelClick?: (e: React.MouseEvent<HTMLElement>) => void
+  className?: string
 }
 
 const VideoPreview: React.FC<Partial<VideoPreviewProps>> = ({
@@ -47,6 +48,7 @@ const VideoPreview: React.FC<Partial<VideoPreviewProps>> = ({
   poster,
   onClick,
   onChannelClick,
+  className,
 }) => {
   const clickable = !!onClick
   const channelClickable = !!onChannelClick
@@ -68,7 +70,7 @@ const VideoPreview: React.FC<Partial<VideoPreviewProps>> = ({
   }
 
   return (
-    <Container onClick={handleClick} clickable={clickable}>
+    <Container onClick={handleClick} clickable={clickable} className={className}>
       <CoverContainer>
         <CoverImage src={poster} ref={imgRef} alt={`${title} by ${channel} thumbnail`} />
         {duration && <CoverDurationOverlay>{duration}</CoverDurationOverlay>}

+ 1 - 0
packages/app/src/shared/components/index.ts

@@ -21,4 +21,5 @@ export { default as ChannelPreview } from './ChannelPreview'
 export { default as HamburgerButton } from './HamburgerButton'
 export { default as Gallery } from './Gallery'
 export { default as Sidenav, SIDENAV_WIDTH, EXPANDED_SIDENAV_WIDTH, NavItem } from './Sidenav'
+export { default as ChannelAvatar } from './ChannelAvatar'
 export { default as GlobalStyle } from './GlobalStyle'

+ 1 - 1
packages/app/src/shared/icons/index.ts

@@ -10,4 +10,4 @@ export { ReactComponent as ChevronRightIcon } from './chevron-right-big.svg'
 export { ReactComponent as ChevronLeftIcon } from './chevron-left-big.svg'
 export { ReactComponent as CheckIcon } from './check.svg'
 export { ReactComponent as DashIcon } from './dash.svg'
-export { ReactComponent as Play } from './play.svg'
+export { ReactComponent as PlayIcon } from './play.svg'

+ 1 - 1
packages/app/src/shared/icons/play.svg

@@ -1 +1 @@
-<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M28.166 23l12 9-12 9V23z" stroke="#fff" stroke-width="3"/><circle cx="32.001" cy="32" r="25.167" stroke="#fff" stroke-width="3"/></svg>
+<svg fill="none" viewBox="5 5 54 54" xmlns="http://www.w3.org/2000/svg"><path d="M28.166 23l12 9-12 9V23z" stroke="#fff" stroke-width="3"/><circle cx="32.001" cy="32" r="25.167" stroke="#fff" stroke-width="3"/></svg>

+ 0 - 27
packages/app/src/shared/stories/13-VideoPreview.stories.tsx

@@ -1,27 +0,0 @@
-import React from 'react'
-import { VideoPreview } from '../components'
-import { boolean, number, text, withKnobs } from '@storybook/addon-knobs'
-import { action } from '@storybook/addon-actions'
-
-export default {
-  title: 'VideoPreview',
-  component: VideoPreview,
-  decorators: [withKnobs],
-}
-
-export const Primary = () => (
-  <VideoPreview
-    title={text('Video title', 'Test video')}
-    channel={text('Channel name', 'Test channel')}
-    channelImg={text('Channel image', '')}
-    showChannel={boolean('Show channel', true)}
-    showMeta={boolean('Show meta', true)}
-    createdAt={text('Formatted time', '2 weeks ago')}
-    duration={text('Video duration', '1:23')}
-    progress={number('Watch progress percentage', 0, { range: true, min: 0, max: 100, step: 1 })}
-    views={text('Views', '30')}
-    poster={text('Poster image', 'https://cdn.pixabay.com/photo/2020/01/31/07/26/japan-4807317_1280.jpg')}
-    onClick={boolean('Clickable', true) ? action('on click') : undefined}
-    onChannelClick={boolean('Channel clickable', true) ? action('on channel click') : undefined}
-  />
-)

+ 28 - 0
packages/app/src/shared/stories/15-VideoPlayer.stories.tsx

@@ -0,0 +1,28 @@
+import React from 'react'
+import { VideoPlayer } from '../components'
+import { boolean, number, text } from '@storybook/addon-knobs'
+
+export default {
+  title: 'VideoPlayer',
+  component: VideoPlayer,
+}
+
+export const Default = () => {
+  const src = text('Video source', 'https://js-video-example.s3.eu-central-1.amazonaws.com/waves.mp4')
+  const autoplay = boolean('Autoplay', true)
+  const fluid = boolean('Fluid mode', false)
+  const fill = boolean('Fill mode', false)
+  const width = number('Width (0 for none)', 800)
+  const height = number('Height (0 for none)', 0)
+
+  return (
+    <VideoPlayer
+      src={src}
+      autoplay={autoplay}
+      fluid={fluid}
+      fill={fill}
+      width={width || undefined}
+      height={height || undefined}
+    />
+  )
+}

+ 27 - 0
packages/app/src/shared/stories/16-VideoPreview.stories.tsx

@@ -0,0 +1,27 @@
+import React from 'react'
+import { VideoPreview } from '../components'
+import { boolean, number, text, withKnobs } from '@storybook/addon-knobs'
+import { action } from '@storybook/addon-actions'
+
+export default {
+  title: 'VideoPreview',
+  component: VideoPreview,
+  decorators: [withKnobs],
+}
+
+export const Primary = () => (
+  <VideoPreview
+    title={text('Video title', 'Test video')}
+    channel={text('Channel name', 'Test channel')}
+    channelImg={text('Channel image', '')}
+    showChannel={boolean('Show channel', true)}
+    showMeta={boolean('Show meta', true)}
+    createdAt={text('Formatted time', '2 weeks ago')}
+    duration={text('Video duration', '1:23')}
+    progress={number('Watch progress percentage', 0, { range: true, min: 0, max: 100, step: 1 })}
+    views={text('Views', '30')}
+    poster={text('Poster image', 'https://cdn.pixabay.com/photo/2020/01/31/07/26/japan-4807317_1280.jpg')}
+    onClick={boolean('Clickable', true) ? action('on click') : undefined}
+    onChannelClick={boolean('Channel clickable', true) ? action('on channel click') : undefined}
+  />
+)

+ 6 - 0
packages/app/src/types/channel.ts

@@ -0,0 +1,6 @@
+type Channel = {
+  name: string
+  avatarUrl?: string
+}
+
+export default Channel

+ 12 - 0
packages/app/src/types/video.ts

@@ -0,0 +1,12 @@
+import { DateTime } from 'luxon'
+import Channel from './channel'
+
+type Video = {
+  title: string
+  views: number
+  createdAt: DateTime
+  description: string
+  channel: Channel
+}
+
+export default Video

+ 5 - 0
packages/app/src/utils/date.ts

@@ -0,0 +1,5 @@
+import { DateTime } from 'luxon'
+
+export const formatDateShort = (date: DateTime): string => {
+  return date.setLocale('en-gb').toLocaleString(DateTime.DATE_MED)
+}

+ 3 - 0
packages/app/src/utils/number.ts

@@ -0,0 +1,3 @@
+export const formatNumber = (num: number): string => {
+  return num.toLocaleString('en-US').split(',').join(' ')
+}

+ 69 - 0
packages/app/src/views/VideoView/VideoView.style.tsx

@@ -0,0 +1,69 @@
+import styled from '@emotion/styled'
+import { ChannelAvatar, VideoPreview } from '@/shared/components'
+import theme from '@/shared/theme'
+
+export const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+`
+
+export const PlayerContainer = styled.div`
+  display: flex;
+  justify-content: center;
+`
+
+export const InfoContainer = styled.div`
+  padding: ${theme.spacing.xxl};
+`
+
+export const TitleActionsContainer = styled.div`
+  display: flex;
+  justify-content: space-between;
+`
+
+export const Title = styled.h2`
+  font-size: ${theme.typography.sizes.h2};
+  margin: 0;
+`
+
+export const ActionsContainer = styled.div``
+
+export const Meta = styled.span`
+  display: block;
+  margin-top: ${theme.spacing.xxs};
+  color: ${theme.colors.gray[300]};
+`
+
+export const StyledChannelAvatar = styled(ChannelAvatar)`
+  margin-top: ${theme.spacing.m};
+`
+
+export const DescriptionContainer = styled.div`
+  margin-top: ${theme.spacing.xl};
+  border-top: 1px solid ${theme.colors.gray[800]};
+
+  p {
+    color: ${theme.colors.gray[300]};
+    line-height: 175%;
+    margin-top: ${theme.spacing.m};
+  }
+`
+
+export const MoreVideosContainer = styled.div`
+  margin-top: 88px;
+`
+
+export const MoreVideosHeader = styled.h5`
+  margin: 0 0 ${theme.spacing.m};
+  font-size: ${theme.typography.sizes.h5};
+`
+
+export const MoreVideosGrid = styled.div`
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+  grid-gap: ${theme.spacing.xl};
+`
+
+export const MoreVideosPreview = styled(VideoPreview)`
+  margin: 0 auto;
+`

+ 75 - 0
packages/app/src/views/VideoView/VideoView.tsx

@@ -0,0 +1,75 @@
+import React from 'react'
+import { RouteComponentProps } from '@reach/router'
+import {
+  ActionsContainer,
+  Container,
+  DescriptionContainer,
+  InfoContainer,
+  Meta,
+  MoreVideosContainer,
+  MoreVideosGrid,
+  MoreVideosHeader,
+  MoreVideosPreview,
+  PlayerContainer,
+  StyledChannelAvatar,
+  Title,
+  TitleActionsContainer,
+} from './VideoView.style'
+import { Button, VideoPlayer } from '@/shared/components'
+import Video from '../../types/video'
+import { DateTime } from 'luxon'
+import { formatDateShort } from '../../utils/date'
+import { formatNumber } from '../../utils/number'
+import { MOCK_VIDEOS } from '../../config/mockData'
+
+const FAKE_VIDEO: Video = {
+  title: 'Sample Video Title',
+  description:
+    'Recounting her story of finding opportunity and stability in the US, Elizabeth Camarillo Gutierrez examines the flaws in narratives that simplify and idealize the immigrant experience -- and shares hard-earned wisdom on the best way to help those around us. "Our world is one that flourishes when different voices come together," she says.\nRecounting her story of finding opportunity and stability in the US, Elizabeth Camarillo Gutierrez examines the flaws in narratives that simplify and idealize the immigrant experience -- and shares hard-earned wisdom on the best way to help those around us. "Our world is one that flourishes when different voices come together," she says.',
+  views: 240737,
+  createdAt: DateTime.local(),
+  channel: {
+    name: 'Channel Name',
+  },
+}
+
+const VideoView: React.FC<RouteComponentProps> = () => {
+  const { title, views, createdAt, channel, description } = FAKE_VIDEO
+
+  const descriptionLines = description.split('\n')
+
+  return (
+    <Container>
+      <PlayerContainer>
+        <VideoPlayer src="https://js-video-example.s3.eu-central-1.amazonaws.com/waves.mp4" height={700} autoplay />
+      </PlayerContainer>
+      <InfoContainer>
+        <TitleActionsContainer>
+          <Title>{title}</Title>
+          <ActionsContainer>
+            <Button type="secondary">Share</Button>
+          </ActionsContainer>
+        </TitleActionsContainer>
+        <Meta>
+          {formatNumber(views)} views • {formatDateShort(createdAt)}
+        </Meta>
+        <StyledChannelAvatar {...channel} />
+        <DescriptionContainer>
+          {descriptionLines.map((line, idx) => (
+            <p key={idx}>{line}</p>
+          ))}
+        </DescriptionContainer>
+        <MoreVideosContainer>
+          <MoreVideosHeader>More from {channel.name}</MoreVideosHeader>
+          <MoreVideosGrid>
+            {MOCK_VIDEOS.map((v, idx) => (
+              <MoreVideosPreview key={idx} {...v} />
+            ))}
+          </MoreVideosGrid>
+        </MoreVideosContainer>
+      </InfoContainer>
+    </Container>
+  )
+}
+
+export default VideoView

+ 1 - 0
packages/app/src/views/VideoView/index.ts

@@ -0,0 +1 @@
+export { default } from './VideoView'

+ 2 - 1
packages/app/src/views/index.ts

@@ -1,3 +1,4 @@
 import HomeView from './HomeView'
+import VideoView from './VideoView'
 
-export { HomeView }
+export { HomeView, VideoView }

+ 168 - 6
yarn.lock

@@ -2698,9 +2698,9 @@
     "@types/node" ">= 8"
 
 "@reach/router@^1.2.1", "@reach/router@^1.3.3":
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c"
-  integrity sha512-+mtn9wjlB9NN2CNnnC/BRYtwdKBfSyyasPYraNAyvaV1occr/5NnB4CVzjEZipNHwYebQwcndGUmpFzxAUoqSA==
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.3.tgz#58162860dce6c9449d49be86b0561b5ef46d80db"
+  integrity sha512-gOIAiFhWdiVGSVjukKeNKkCRBLmnORoTPyBihI/jLunICPgxdP30DroAvPQuf1eVfQbfGJQDJkwhJXsNPMnVWw==
   dependencies:
     create-react-context "0.3.0"
     invariant "^2.2.3"
@@ -3420,6 +3420,11 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
   integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
 
+"@types/luxon@^1.24.1":
+  version "1.24.1"
+  resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.24.1.tgz#60f57209b9afd8e046161ecb8322e2875fc182ee"
+  integrity sha512-t93qL4l3PRxy4qQkXwiPS6qjIt7S6o90XMuCfvYDgIAQJvgSBni5qVPxkhGYPnZZPS9ASHOTeCZXRNmdiHHHcg==
+
 "@types/mdast@^3.0.0":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
@@ -3573,6 +3578,11 @@
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
   integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
 
+"@types/video.js@^7.3.10":
+  version "7.3.10"
+  resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.10.tgz#95e563f5f21821d4f4bccf7343c359093e81895d"
+  integrity sha512-RJKdtmUMb8WFDe+btAeBW9NVUJ/nksb+/1S+Tca7HNwnRv9iEwt802hgRSAcFj6+sL/eNi264zcBjbBHP5yQVw==
+
 "@types/webpack-env@^1.15.0":
   version "1.15.2"
   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a"
@@ -3721,6 +3731,37 @@
   dependencies:
     eslint-visitor-keys "^1.1.0"
 
+"@videojs/http-streaming@1.13.2":
+  version "1.13.2"
+  resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-1.13.2.tgz#9e91f9f440ccaf6c8ed640a3614216397bb38558"
+  integrity sha512-U4Xhh+HxGpRBx9Gm0LlEadq85k9BwckzFgZmyhacauhK/27Mz0goKKFAt+BpxBNp2oHVdAdk8NHfneinsqni3Q==
+  dependencies:
+    aes-decrypter "3.0.0"
+    global "^4.3.0"
+    m3u8-parser "4.4.0"
+    mpd-parser "0.10.0"
+    mux.js "5.5.1"
+    url-toolkit "^2.1.3"
+    video.js "^6.8.0 || ^7.0.0"
+
+"@videojs/vhs-utils@^1.1.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-1.3.0.tgz#04fe402f603af9a5df4b88881fabba0cf13814c2"
+  integrity sha512-oiqXDtHQqDPun7JseWkirUHGrgdYdeF12goUut5z7vwAj4DmUufEPFJ4xK5hYGXGFDyDhk2rSFOR122Ze6qXyQ==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    global "^4.3.2"
+    url-toolkit "^2.1.6"
+
+"@videojs/xhr@2.5.1":
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/@videojs/xhr/-/xhr-2.5.1.tgz#26bc5a79dbb3b03bfb13742c6ce559f89e90719e"
+  integrity sha512-wV9nGESHseSK+S9ePEru2+OJZ1jq/ZbbzniGQ4weAmTIepuBMSYPx5zrxxQA0E786T5ykpO8ts+LayV+3/oI2w==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    global "~4.4.0"
+    is-function "^1.0.1"
+
 "@webassemblyjs/ast@1.8.5":
   version "1.8.5"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
@@ -4148,6 +4189,15 @@ adjust-sourcemap-loader@2.0.0:
     object-path "0.11.4"
     regex-parser "2.2.10"
 
+aes-decrypter@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-3.0.0.tgz#7848a1c145b9fdbf57ae3e2b5b1bc7cf0644a8fb"
+  integrity sha1-eEihwUW5/b9Xrj4rWxvHzwZEqPs=
+  dependencies:
+    commander "^2.9.0"
+    global "^4.3.2"
+    pkcs7 "^1.0.2"
+
 agent-base@4, agent-base@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
@@ -6148,7 +6198,7 @@ comma-separated-tokens@^1.0.0:
   resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
   integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
 
-commander@^2.11.0, commander@^2.19.0, commander@^2.20.0:
+commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -9003,7 +9053,15 @@ global-prefix@^3.0.0:
     kind-of "^6.0.2"
     which "^1.3.1"
 
-global@^4.3.2, global@^4.4.0:
+global@4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
+  integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=
+  dependencies:
+    min-document "^2.19.0"
+    process "~0.5.1"
+
+global@^4.3.0, global@^4.3.1, global@^4.3.2, global@^4.4.0, global@~4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
   integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
@@ -9699,6 +9757,11 @@ indexes-of@^1.0.1:
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
   integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
 
+individual@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/individual/-/individual-2.0.0.tgz#833b097dad23294e76117a98fb38e0d9ad61bb97"
+  integrity sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c=
+
 infer-owner@^1.0.3, infer-owner@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
@@ -11546,6 +11609,11 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1:
     array-includes "^3.1.1"
     object.assign "^4.1.0"
 
+keycode@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
+  integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=
+
 killable@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -12025,6 +12093,18 @@ lru-cache@^5.1.1:
   dependencies:
     yallist "^3.0.2"
 
+luxon@^1.24.1:
+  version "1.24.1"
+  resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.24.1.tgz#a8383266131ed4eaed4b5f430f96f3695403a52a"
+  integrity sha512-CgnIMKAWT0ghcuWFfCWBnWGOddM0zu6c4wZAWmD0NN7MZTnro0+833DF6tJep+xlxRPg4KtsYEHYLfTMBQKwYg==
+
+m3u8-parser@4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.4.0.tgz#adf606c0af6d97f6750095a42006c2ae03dde177"
+  integrity sha512-iH2AygTFILtato+XAgnoPYzLHM4R3DjATj7Ozbk7EHdB2XoLF2oyOUguM7Kc4UVHbQHHL/QPaw98r7PbWzG0gg==
+  dependencies:
+    global "^4.3.2"
+
 macos-release@^2.2.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e"
@@ -12607,6 +12687,16 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     run-queue "^1.0.3"
 
+mpd-parser@0.10.0:
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.10.0.tgz#e48a39a4ecd3b5bbd0ed4ac5991b9cc36bcd9599"
+  integrity sha512-eIqkH/2osPr7tIIjhRmDWqm2wdJ7Q8oPfWvdjealzsLV2D2oNe0a0ae2gyYYs1sw5e5hdssDA2V6Sz8MW+Uvvw==
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@videojs/vhs-utils" "^1.1.0"
+    global "^4.3.2"
+    xmldom "^0.1.27"
+
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -12655,6 +12745,11 @@ mute-stream@0.0.8, mute-stream@~0.0.4:
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
   integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
+mux.js@5.5.1:
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-5.5.1.tgz#5bd5d7b2e5e5560da8126928e93af3c532e08372"
+  integrity sha512-5VmmjADBqS4++8pTI6poSRJ+chHdaoI4XErcQPM5w4QfwaDl+FQlSI0iOgWbYDn6CBCbDRKaSCcEiN2K5aHNGQ==
+
 mz@^2.5.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
@@ -13702,6 +13797,11 @@ pirates@^4.0.0, pirates@^4.0.1:
   dependencies:
     node-modules-regexp "^1.0.0"
 
+pkcs7@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pkcs7/-/pkcs7-1.0.2.tgz#b6dba527528c2942bfc122ce2dafcdb5e59074e7"
+  integrity sha1-ttulJ1KMKUK/wSLOLa/NteWQdOc=
+
 pkg-dir@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
@@ -14558,6 +14658,11 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
 
+process@~0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+  integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
+
 progress-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-2.0.0.tgz#fac63a0b3d11deacbb0969abcc93b214bce19ed5"
@@ -16120,7 +16225,21 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
-rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5, rxjs@^6.6.0:
+rust-result@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72"
+  integrity sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=
+  dependencies:
+    individual "^2.0.0"
+
+rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.3, rxjs@^6.5.5:
+  version "6.5.5"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
+  integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
+  dependencies:
+    tslib "^1.9.0"
+
+rxjs@^6.6.0:
   version "6.6.0"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84"
   integrity sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==
@@ -16142,6 +16261,13 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1,
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
+safe-json-parse@4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-4.0.0.tgz#7c0f578cfccd12d33a71c0e05413e2eca171eaac"
+  integrity sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=
+  dependencies:
+    rust-result "^1.0.0"
+
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -18054,6 +18180,11 @@ url-parse@^1.4.3:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
 
+url-toolkit@^2.1.3, url-toolkit@^2.1.6:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.0.tgz#9a57b89f315d4b7dc340e150bcfa548ddf5f5ce9"
+  integrity sha512-Rde0c9S4fJK3FaHim3DSgdQ8IFrSXcZCpAJo9T7/FA+BoQGhK0ow3mpwGQLJCPYsNn6TstpW7/7DzMpSpz9F9w==
+
 url@^0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -18223,6 +18354,32 @@ vfile@^4.0.0:
     unist-util-stringify-position "^2.0.0"
     vfile-message "^2.0.0"
 
+"video.js@^6.8.0 || ^7.0.0", video.js@^7.8.3:
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.8.3.tgz#a32df2b8e5f8d2f6b06d6ccd3bf91c6342a2a365"
+  integrity sha512-u8/1qEZdBeOm7TgBhJg8ab28vd3x62UMaaSnZ79yOMaxCqACP9CzWJT9c3Isfv2jY9BNLBIIft+BqNLTWudtLw==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+    "@videojs/http-streaming" "1.13.2"
+    "@videojs/xhr" "2.5.1"
+    global "4.3.2"
+    keycode "^2.2.0"
+    safe-json-parse "4.0.0"
+    videojs-font "3.2.0"
+    videojs-vtt.js "^0.15.2"
+
+videojs-font@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232"
+  integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==
+
+videojs-vtt.js@^0.15.2:
+  version "0.15.2"
+  resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.2.tgz#a828c4ea0aac6303fa471fd69bc7586a5ba1a273"
+  integrity sha512-kEo4hNMvu+6KhPvVYPKwESruwhHC3oFis133LwhXHO9U7nRnx0RiJYMiqbgwjgazDEXHR6t8oGJiHM6wq5XlAw==
+  dependencies:
+    global "^4.3.1"
+
 vm-browserify@^1.0.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
@@ -18923,6 +19080,11 @@ xmlchars@^2.1.1, xmlchars@^2.2.0:
   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
 
+xmldom@^0.1.27:
+  version "0.1.31"
+  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
+  integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
+
 xregexp@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"