Browse Source

chore: update video player

murray 5 years ago
parent
commit
df24ea5cd1
64 changed files with 4339 additions and 61 deletions
  1. 93
    0
      config/env.js
  2. 14
    0
      config/jest/cssTransform.js
  3. 40
    0
      config/jest/fileTransform.js
  4. 141
    0
      config/modules.js
  5. 90
    0
      config/paths.js
  6. 35
    0
      config/pnpTs.js
  7. 673
    0
      config/webpack.config.js
  8. 110
    0
      config/webpackDevServer.config.js
  9. 117
    10
      package.json
  10. 211
    0
      scripts/build.js
  11. 147
    0
      scripts/start.js
  12. 53
    0
      scripts/test.js
  13. 17
    29
      src/App.css
  14. 54
    21
      src/App.js
  15. BIN
      src/assets/1.mp4
  16. BIN
      src/assets/1.png
  17. 35
    0
      src/components/DefaultPlayer/Captions/Captions.js
  18. 56
    0
      src/components/DefaultPlayer/Captions/Captions.module.css
  19. 32
    0
      src/components/DefaultPlayer/Captions/Captions.test.js
  20. 200
    0
      src/components/DefaultPlayer/DefaultPlayer.js
  21. 31
    0
      src/components/DefaultPlayer/DefaultPlayer.module.css
  22. 108
    0
      src/components/DefaultPlayer/DefaultPlayer.test.js
  23. 18
    0
      src/components/DefaultPlayer/Fullscreen/Fullscreen.js
  24. 25
    0
      src/components/DefaultPlayer/Fullscreen/Fullscreen.module.css
  25. 44
    0
      src/components/DefaultPlayer/Fullscreen/Fullscreen.test.js
  26. 6
    0
      src/components/DefaultPlayer/Icon/caption_new.svg
  27. 6
    0
      src/components/DefaultPlayer/Icon/closed_caption.svg
  28. 6
    0
      src/components/DefaultPlayer/Icon/fullscreen.svg
  29. 6
    0
      src/components/DefaultPlayer/Icon/fullscreen_exit.svg
  30. 6
    0
      src/components/DefaultPlayer/Icon/pause.svg
  31. 6
    0
      src/components/DefaultPlayer/Icon/play_arrow.svg
  32. 6
    0
      src/components/DefaultPlayer/Icon/report.svg
  33. 6
    0
      src/components/DefaultPlayer/Icon/speed.svg
  34. 1
    0
      src/components/DefaultPlayer/Icon/spin.svg
  35. 6
    0
      src/components/DefaultPlayer/Icon/volume_down.svg
  36. 6
    0
      src/components/DefaultPlayer/Icon/volume_mute.svg
  37. 6
    0
      src/components/DefaultPlayer/Icon/volume_off.svg
  38. 6
    0
      src/components/DefaultPlayer/Icon/volume_up.svg
  39. 48
    0
      src/components/DefaultPlayer/Overlay/Overlay.js
  40. 26
    0
      src/components/DefaultPlayer/Overlay/Overlay.module.css
  41. 56
    0
      src/components/DefaultPlayer/Overlay/Overlay.test.js
  42. 29
    0
      src/components/DefaultPlayer/PlayPause/PlayPause.js
  43. 25
    0
      src/components/DefaultPlayer/PlayPause/PlayPause.module.css
  44. 83
    0
      src/components/DefaultPlayer/PlayPause/PlayPause.test.js
  45. 40
    0
      src/components/DefaultPlayer/Seek/Seek.js
  46. 36
    0
      src/components/DefaultPlayer/Seek/Seek.module.css
  47. 91
    0
      src/components/DefaultPlayer/Seek/Seek.test.js
  48. 41
    0
      src/components/DefaultPlayer/Speed/Speed.js
  49. 58
    0
      src/components/DefaultPlayer/Speed/Speed.module.css
  50. 32
    0
      src/components/DefaultPlayer/Speed/Speed.test.js
  51. 22
    0
      src/components/DefaultPlayer/Time/Time.js
  52. 14
    0
      src/components/DefaultPlayer/Time/Time.module.css
  53. 91
    0
      src/components/DefaultPlayer/Time/Time.test.js
  54. 65
    0
      src/components/DefaultPlayer/Volume/Volume.js
  55. 77
    0
      src/components/DefaultPlayer/Volume/Volume.module.css
  56. 165
    0
      src/components/DefaultPlayer/Volume/Volume.test.js
  57. 12
    0
      src/components/DefaultPlayer/copy.js
  58. 24
    0
      src/components/entry.js
  59. 145
    0
      src/components/video/api.js
  60. 276
    0
      src/components/video/api.test.js
  61. 71
    0
      src/components/video/constants.js
  62. 153
    0
      src/components/video/video.js
  63. 234
    0
      src/components/video/video.test.js
  64. 8
    1
      yarn.lock

+ 93
- 0
config/env.js View File

@@ -0,0 +1,93 @@
1
+'use strict';
2
+
3
+const fs = require('fs');
4
+const path = require('path');
5
+const paths = require('./paths');
6
+
7
+// Make sure that including paths.js after env.js will read .env variables.
8
+delete require.cache[require.resolve('./paths')];
9
+
10
+const NODE_ENV = process.env.NODE_ENV;
11
+if (!NODE_ENV) {
12
+  throw new Error(
13
+    'The NODE_ENV environment variable is required but was not specified.'
14
+  );
15
+}
16
+
17
+// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
18
+const dotenvFiles = [
19
+  `${paths.dotenv}.${NODE_ENV}.local`,
20
+  `${paths.dotenv}.${NODE_ENV}`,
21
+  // Don't include `.env.local` for `test` environment
22
+  // since normally you expect tests to produce the same
23
+  // results for everyone
24
+  NODE_ENV !== 'test' && `${paths.dotenv}.local`,
25
+  paths.dotenv,
26
+].filter(Boolean);
27
+
28
+// Load environment variables from .env* files. Suppress warnings using silent
29
+// if this file is missing. dotenv will never modify any environment variables
30
+// that have already been set.  Variable expansion is supported in .env files.
31
+// https://github.com/motdotla/dotenv
32
+// https://github.com/motdotla/dotenv-expand
33
+dotenvFiles.forEach(dotenvFile => {
34
+  if (fs.existsSync(dotenvFile)) {
35
+    require('dotenv-expand')(
36
+      require('dotenv').config({
37
+        path: dotenvFile,
38
+      })
39
+    );
40
+  }
41
+});
42
+
43
+// We support resolving modules according to `NODE_PATH`.
44
+// This lets you use absolute paths in imports inside large monorepos:
45
+// https://github.com/facebook/create-react-app/issues/253.
46
+// It works similar to `NODE_PATH` in Node itself:
47
+// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
48
+// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
49
+// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
50
+// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
51
+// We also resolve them to make sure all tools using them work consistently.
52
+const appDirectory = fs.realpathSync(process.cwd());
53
+process.env.NODE_PATH = (process.env.NODE_PATH || '')
54
+  .split(path.delimiter)
55
+  .filter(folder => folder && !path.isAbsolute(folder))
56
+  .map(folder => path.resolve(appDirectory, folder))
57
+  .join(path.delimiter);
58
+
59
+// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
60
+// injected into the application via DefinePlugin in Webpack configuration.
61
+const REACT_APP = /^REACT_APP_/i;
62
+
63
+function getClientEnvironment(publicUrl) {
64
+  const raw = Object.keys(process.env)
65
+    .filter(key => REACT_APP.test(key))
66
+    .reduce(
67
+      (env, key) => {
68
+        env[key] = process.env[key];
69
+        return env;
70
+      },
71
+      {
72
+        // Useful for determining whether we’re running in production mode.
73
+        // Most importantly, it switches React into the correct mode.
74
+        NODE_ENV: process.env.NODE_ENV || 'development',
75
+        // Useful for resolving the correct path to static assets in `public`.
76
+        // For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
77
+        // This should only be used as an escape hatch. Normally you would put
78
+        // images into the `src` and `import` them in code to get their paths.
79
+        PUBLIC_URL: publicUrl,
80
+      }
81
+    );
82
+  // Stringify all values so we can feed into Webpack DefinePlugin
83
+  const stringified = {
84
+    'process.env': Object.keys(raw).reduce((env, key) => {
85
+      env[key] = JSON.stringify(raw[key]);
86
+      return env;
87
+    }, {}),
88
+  };
89
+
90
+  return { raw, stringified };
91
+}
92
+
93
+module.exports = getClientEnvironment;

+ 14
- 0
config/jest/cssTransform.js View File

@@ -0,0 +1,14 @@
1
+'use strict';
2
+
3
+// This is a custom Jest transformer turning style imports into empty objects.
4
+// http://facebook.github.io/jest/docs/en/webpack.html
5
+
6
+module.exports = {
7
+  process() {
8
+    return 'module.exports = {};';
9
+  },
10
+  getCacheKey() {
11
+    // The output is always the same.
12
+    return 'cssTransform';
13
+  },
14
+};

+ 40
- 0
config/jest/fileTransform.js View File

@@ -0,0 +1,40 @@
1
+'use strict';
2
+
3
+const path = require('path');
4
+const camelcase = require('camelcase');
5
+
6
+// This is a custom Jest transformer turning file imports into filenames.
7
+// http://facebook.github.io/jest/docs/en/webpack.html
8
+
9
+module.exports = {
10
+  process(src, filename) {
11
+    const assetFilename = JSON.stringify(path.basename(filename));
12
+
13
+    if (filename.match(/\.svg$/)) {
14
+      // Based on how SVGR generates a component name:
15
+      // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16
+      const pascalCaseFilename = camelcase(path.parse(filename).name, {
17
+        pascalCase: true,
18
+      });
19
+      const componentName = `Svg${pascalCaseFilename}`;
20
+      return `const React = require('react');
21
+      module.exports = {
22
+        __esModule: true,
23
+        default: ${assetFilename},
24
+        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25
+          return {
26
+            $$typeof: Symbol.for('react.element'),
27
+            type: 'svg',
28
+            ref: ref,
29
+            key: null,
30
+            props: Object.assign({}, props, {
31
+              children: ${assetFilename}
32
+            })
33
+          };
34
+        }),
35
+      };`;
36
+    }
37
+
38
+    return `module.exports = ${assetFilename};`;
39
+  },
40
+};

+ 141
- 0
config/modules.js View File

@@ -0,0 +1,141 @@
1
+'use strict';
2
+
3
+const fs = require('fs');
4
+const path = require('path');
5
+const paths = require('./paths');
6
+const chalk = require('react-dev-utils/chalk');
7
+const resolve = require('resolve');
8
+
9
+/**
10
+ * Get additional module paths based on the baseUrl of a compilerOptions object.
11
+ *
12
+ * @param {Object} options
13
+ */
14
+function getAdditionalModulePaths(options = {}) {
15
+  const baseUrl = options.baseUrl;
16
+
17
+  // We need to explicitly check for null and undefined (and not a falsy value) because
18
+  // TypeScript treats an empty string as `.`.
19
+  if (baseUrl == null) {
20
+    // If there's no baseUrl set we respect NODE_PATH
21
+    // Note that NODE_PATH is deprecated and will be removed
22
+    // in the next major release of create-react-app.
23
+
24
+    const nodePath = process.env.NODE_PATH || '';
25
+    return nodePath.split(path.delimiter).filter(Boolean);
26
+  }
27
+
28
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
29
+
30
+  // We don't need to do anything if `baseUrl` is set to `node_modules`. This is
31
+  // the default behavior.
32
+  if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
33
+    return null;
34
+  }
35
+
36
+  // Allow the user set the `baseUrl` to `appSrc`.
37
+  if (path.relative(paths.appSrc, baseUrlResolved) === '') {
38
+    return [paths.appSrc];
39
+  }
40
+
41
+  // If the path is equal to the root directory we ignore it here.
42
+  // We don't want to allow importing from the root directly as source files are
43
+  // not transpiled outside of `src`. We do allow importing them with the
44
+  // absolute path (e.g. `src/Components/Button.js`) but we set that up with
45
+  // an alias.
46
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
47
+    return null;
48
+  }
49
+
50
+  // Otherwise, throw an error.
51
+  throw new Error(
52
+    chalk.red.bold(
53
+      "Your project's `baseUrl` can only be set to `src` or `node_modules`." +
54
+        ' Create React App does not support other values at this time.'
55
+    )
56
+  );
57
+}
58
+
59
+/**
60
+ * Get webpack aliases based on the baseUrl of a compilerOptions object.
61
+ *
62
+ * @param {*} options
63
+ */
64
+function getWebpackAliases(options = {}) {
65
+  const baseUrl = options.baseUrl;
66
+
67
+  if (!baseUrl) {
68
+    return {};
69
+  }
70
+
71
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
72
+
73
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
74
+    return {
75
+      src: paths.appSrc,
76
+    };
77
+  }
78
+}
79
+
80
+/**
81
+ * Get jest aliases based on the baseUrl of a compilerOptions object.
82
+ *
83
+ * @param {*} options
84
+ */
85
+function getJestAliases(options = {}) {
86
+  const baseUrl = options.baseUrl;
87
+
88
+  if (!baseUrl) {
89
+    return {};
90
+  }
91
+
92
+  const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
93
+
94
+  if (path.relative(paths.appPath, baseUrlResolved) === '') {
95
+    return {
96
+      '^src/(.*)$': '<rootDir>/src/$1',
97
+    };
98
+  }
99
+}
100
+
101
+function getModules() {
102
+  // Check if TypeScript is setup
103
+  const hasTsConfig = fs.existsSync(paths.appTsConfig);
104
+  const hasJsConfig = fs.existsSync(paths.appJsConfig);
105
+
106
+  if (hasTsConfig && hasJsConfig) {
107
+    throw new Error(
108
+      'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
109
+    );
110
+  }
111
+
112
+  let config;
113
+
114
+  // If there's a tsconfig.json we assume it's a
115
+  // TypeScript project and set up the config
116
+  // based on tsconfig.json
117
+  if (hasTsConfig) {
118
+    const ts = require(resolve.sync('typescript', {
119
+      basedir: paths.appNodeModules,
120
+    }));
121
+    config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
122
+    // Otherwise we'll check if there is jsconfig.json
123
+    // for non TS projects.
124
+  } else if (hasJsConfig) {
125
+    config = require(paths.appJsConfig);
126
+  }
127
+
128
+  config = config || {};
129
+  const options = config.compilerOptions || {};
130
+
131
+  const additionalModulePaths = getAdditionalModulePaths(options);
132
+
133
+  return {
134
+    additionalModulePaths: additionalModulePaths,
135
+    webpackAliases: getWebpackAliases(options),
136
+    jestAliases: getJestAliases(options),
137
+    hasTsConfig,
138
+  };
139
+}
140
+
141
+module.exports = getModules();

+ 90
- 0
config/paths.js View File

@@ -0,0 +1,90 @@
1
+'use strict';
2
+
3
+const path = require('path');
4
+const fs = require('fs');
5
+const url = require('url');
6
+
7
+// Make sure any symlinks in the project folder are resolved:
8
+// https://github.com/facebook/create-react-app/issues/637
9
+const appDirectory = fs.realpathSync(process.cwd());
10
+const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11
+
12
+const envPublicUrl = process.env.PUBLIC_URL;
13
+
14
+function ensureSlash(inputPath, needsSlash) {
15
+  const hasSlash = inputPath.endsWith('/');
16
+  if (hasSlash && !needsSlash) {
17
+    return inputPath.substr(0, inputPath.length - 1);
18
+  } else if (!hasSlash && needsSlash) {
19
+    return `${inputPath}/`;
20
+  } else {
21
+    return inputPath;
22
+  }
23
+}
24
+
25
+const getPublicUrl = appPackageJson =>
26
+  envPublicUrl || require(appPackageJson).homepage;
27
+
28
+// We use `PUBLIC_URL` environment variable or "homepage" field to infer
29
+// "public path" at which the app is served.
30
+// Webpack needs to know it to put the right <script> hrefs into HTML even in
31
+// single-page apps that may serve index.html for nested URLs like /todos/42.
32
+// We can't use a relative path in HTML because we don't want to load something
33
+// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
34
+function getServedPath(appPackageJson) {
35
+  const publicUrl = getPublicUrl(appPackageJson);
36
+  const servedUrl =
37
+    envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
38
+  return ensureSlash(servedUrl, true);
39
+}
40
+
41
+const moduleFileExtensions = [
42
+  'web.mjs',
43
+  'mjs',
44
+  'web.js',
45
+  'js',
46
+  'web.ts',
47
+  'ts',
48
+  'web.tsx',
49
+  'tsx',
50
+  'json',
51
+  'web.jsx',
52
+  'jsx',
53
+];
54
+
55
+// Resolve file paths in the same order as webpack
56
+const resolveModule = (resolveFn, filePath) => {
57
+  const extension = moduleFileExtensions.find(extension =>
58
+    fs.existsSync(resolveFn(`${filePath}.${extension}`))
59
+  );
60
+
61
+  if (extension) {
62
+    return resolveFn(`${filePath}.${extension}`);
63
+  }
64
+
65
+  return resolveFn(`${filePath}.js`);
66
+};
67
+
68
+// config after eject: we're in ./config/
69
+module.exports = {
70
+  dotenv: resolveApp('.env'),
71
+  appPath: resolveApp('.'),
72
+  appBuild: resolveApp('build'),
73
+  appPublic: resolveApp('public'),
74
+  appHtml: resolveApp('public/index.html'),
75
+  appIndexJs: resolveModule(resolveApp, 'src/index'),
76
+  appPackageJson: resolveApp('package.json'),
77
+  appSrc: resolveApp('src'),
78
+  appTsConfig: resolveApp('tsconfig.json'),
79
+  appJsConfig: resolveApp('jsconfig.json'),
80
+  yarnLockFile: resolveApp('yarn.lock'),
81
+  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
82
+  proxySetup: resolveApp('src/setupProxy.js'),
83
+  appNodeModules: resolveApp('node_modules'),
84
+  publicUrl: getPublicUrl(resolveApp('package.json')),
85
+  servedPath: getServedPath(resolveApp('package.json')),
86
+};
87
+
88
+
89
+
90
+module.exports.moduleFileExtensions = moduleFileExtensions;

+ 35
- 0
config/pnpTs.js View File

@@ -0,0 +1,35 @@
1
+'use strict';
2
+
3
+const { resolveModuleName } = require('ts-pnp');
4
+
5
+exports.resolveModuleName = (
6
+  typescript,
7
+  moduleName,
8
+  containingFile,
9
+  compilerOptions,
10
+  resolutionHost
11
+) => {
12
+  return resolveModuleName(
13
+    moduleName,
14
+    containingFile,
15
+    compilerOptions,
16
+    resolutionHost,
17
+    typescript.resolveModuleName
18
+  );
19
+};
20
+
21
+exports.resolveTypeReferenceDirective = (
22
+  typescript,
23
+  moduleName,
24
+  containingFile,
25
+  compilerOptions,
26
+  resolutionHost
27
+) => {
28
+  return resolveModuleName(
29
+    moduleName,
30
+    containingFile,
31
+    compilerOptions,
32
+    resolutionHost,
33
+    typescript.resolveTypeReferenceDirective
34
+  );
35
+};

+ 673
- 0
config/webpack.config.js View File

@@ -0,0 +1,673 @@
1
+'use strict';
2
+
3
+const fs = require('fs');
4
+const path = require('path');
5
+const webpack = require('webpack');
6
+const resolve = require('resolve');
7
+const PnpWebpackPlugin = require('pnp-webpack-plugin');
8
+const HtmlWebpackPlugin = require('html-webpack-plugin');
9
+const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
10
+const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
11
+const TerserPlugin = require('terser-webpack-plugin');
12
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
13
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
14
+const safePostCssParser = require('postcss-safe-parser');
15
+const ManifestPlugin = require('webpack-manifest-plugin');
16
+const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
17
+const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
18
+const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
19
+const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
20
+const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
21
+const paths = require('./paths');
22
+const modules = require('./modules');
23
+const getClientEnvironment = require('./env');
24
+const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
25
+const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
26
+const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
27
+
28
+const postcssNormalize = require('postcss-normalize');
29
+
30
+const appPackageJson = require(paths.appPackageJson);
31
+
32
+// Source maps are resource heavy and can cause out of memory issue for large source files.
33
+const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
34
+// Some apps do not need the benefits of saving a web request, so not inlining the chunk
35
+// makes for a smoother build process.
36
+const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
37
+
38
+const imageInlineSizeLimit = parseInt(
39
+  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
40
+);
41
+
42
+// Check if TypeScript is setup
43
+const useTypeScript = fs.existsSync(paths.appTsConfig);
44
+
45
+// style files regexes
46
+const cssRegex = /\.css$/;
47
+const cssModuleRegex = /\.module\.css$/;
48
+const sassRegex = /\.(scss|sass)$/;
49
+const sassModuleRegex = /\.module\.(scss|sass)$/;
50
+
51
+// This is the production and development configuration.
52
+// It is focused on developer experience, fast rebuilds, and a minimal bundle.
53
+module.exports = function(webpackEnv) {
54
+  const isEnvDevelopment = webpackEnv === 'development';
55
+  const isEnvProduction = webpackEnv === 'production';
56
+
57
+  // Variable used for enabling profiling in Production
58
+  // passed into alias object. Uses a flag if passed into the build command
59
+  const isEnvProductionProfile =
60
+    isEnvProduction && process.argv.includes('--profile');
61
+
62
+  // Webpack uses `publicPath` to determine where the app is being served from.
63
+  // It requires a trailing slash, or the file assets will get an incorrect path.
64
+  // In development, we always serve from the root. This makes config easier.
65
+  const publicPath = isEnvProduction
66
+    ? paths.servedPath
67
+    : isEnvDevelopment && '/';
68
+  // Some apps do not use client-side routing with pushState.
69
+  // For these, "homepage" can be set to "." to enable relative asset paths.
70
+  const shouldUseRelativeAssetPaths = publicPath === './';
71
+
72
+  // `publicUrl` is just like `publicPath`, but we will provide it to our app
73
+  // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
74
+  // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
75
+  const publicUrl = isEnvProduction
76
+    ? publicPath.slice(0, -1)
77
+    : isEnvDevelopment && '';
78
+  // Get environment variables to inject into our app.
79
+  const env = getClientEnvironment(publicUrl);
80
+
81
+  // common function to get style loaders
82
+  const getStyleLoaders = (cssOptions, preProcessor) => {
83
+    const loaders = [
84
+      isEnvDevelopment && require.resolve('style-loader'),
85
+      isEnvProduction && {
86
+        loader: MiniCssExtractPlugin.loader,
87
+        options: shouldUseRelativeAssetPaths ? { publicPath: '../../' } : {},
88
+      },
89
+      {
90
+        loader: require.resolve('css-loader'),
91
+        options: cssOptions,
92
+      },
93
+      {
94
+        // Options for PostCSS as we reference these options twice
95
+        // Adds vendor prefixing based on your specified browser support in
96
+        // package.json
97
+        loader: require.resolve('postcss-loader'),
98
+        options: {
99
+          // Necessary for external CSS imports to work
100
+          // https://github.com/facebook/create-react-app/issues/2677
101
+          ident: 'postcss',
102
+          plugins: () => [
103
+            require('postcss-flexbugs-fixes'),
104
+            require('postcss-preset-env')({
105
+              autoprefixer: {
106
+                flexbox: 'no-2009',
107
+              },
108
+              stage: 3,
109
+            }),
110
+            // Adds PostCSS Normalize as the reset css with default options,
111
+            // so that it honors browserslist config in package.json
112
+            // which in turn let's users customize the target behavior as per their needs.
113
+            postcssNormalize(),
114
+          ],
115
+          sourceMap: isEnvProduction && shouldUseSourceMap,
116
+        },
117
+      },
118
+    ].filter(Boolean);
119
+    if (preProcessor) {
120
+      loaders.push(
121
+        {
122
+          loader: require.resolve('resolve-url-loader'),
123
+          options: {
124
+            sourceMap: isEnvProduction && shouldUseSourceMap,
125
+          },
126
+        },
127
+        {
128
+          loader: require.resolve(preProcessor),
129
+          options: {
130
+            sourceMap: true,
131
+          },
132
+        }
133
+      );
134
+    }
135
+    return loaders;
136
+  };
137
+
138
+  return {
139
+    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
140
+    // Stop compilation early in production
141
+    bail: isEnvProduction,
142
+    devtool: isEnvProduction
143
+      ? shouldUseSourceMap
144
+        ? 'source-map'
145
+        : false
146
+      : isEnvDevelopment && 'cheap-module-source-map',
147
+    // These are the "entry points" to our application.
148
+    // This means they will be the "root" imports that are included in JS bundle.
149
+    entry: [
150
+      // Include an alternative client for WebpackDevServer. A client's job is to
151
+      // connect to WebpackDevServer by a socket and get notified about changes.
152
+      // When you save a file, the client will either apply hot updates (in case
153
+      // of CSS changes), or refresh the page (in case of JS changes). When you
154
+      // make a syntax error, this client will display a syntax error overlay.
155
+      // Note: instead of the default WebpackDevServer client, we use a custom one
156
+      // to bring better experience for Create React App users. You can replace
157
+      // the line below with these two lines if you prefer the stock client:
158
+      // require.resolve('webpack-dev-server/client') + '?/',
159
+      // require.resolve('webpack/hot/dev-server'),
160
+      isEnvDevelopment &&
161
+        require.resolve('react-dev-utils/webpackHotDevClient'),
162
+      // Finally, this is your app's code:
163
+      paths.appIndexJs,
164
+      // We include the app code last so that if there is a runtime error during
165
+      // initialization, it doesn't blow up the WebpackDevServer client, and
166
+      // changing JS code would still trigger a refresh.
167
+    ].filter(Boolean),
168
+    output: {
169
+      // The build folder.
170
+      path: isEnvProduction ? paths.appBuild : undefined,
171
+      // Add /* filename */ comments to generated require()s in the output.
172
+      pathinfo: isEnvDevelopment,
173
+      // There will be one main bundle, and one file per asynchronous chunk.
174
+      // In development, it does not produce real files.
175
+      filename: isEnvProduction
176
+        ? 'static/js/[name].[contenthash:8].js'
177
+        : isEnvDevelopment && 'static/js/bundle.js',
178
+      // TODO: remove this when upgrading to webpack 5
179
+      futureEmitAssets: true,
180
+      // There are also additional JS chunk files if you use code splitting.
181
+      chunkFilename: isEnvProduction
182
+        ? 'static/js/[name].[contenthash:8].chunk.js'
183
+        : isEnvDevelopment && 'static/js/[name].chunk.js',
184
+      // We inferred the "public path" (such as / or /my-project) from homepage.
185
+      // We use "/" in development.
186
+      publicPath: publicPath,
187
+      // Point sourcemap entries to original disk location (format as URL on Windows)
188
+      devtoolModuleFilenameTemplate: isEnvProduction
189
+        ? info =>
190
+            path
191
+              .relative(paths.appSrc, info.absoluteResourcePath)
192
+              .replace(/\\/g, '/')
193
+        : isEnvDevelopment &&
194
+          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
195
+      // Prevents conflicts when multiple Webpack runtimes (from different apps)
196
+      // are used on the same page.
197
+      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
198
+      // this defaults to 'window', but by setting it to 'this' then
199
+      // module chunks which are built will work in web workers as well.
200
+      globalObject: 'this',
201
+    },
202
+    optimization: {
203
+      minimize: isEnvProduction,
204
+      minimizer: [
205
+        // This is only used in production mode
206
+        new TerserPlugin({
207
+          terserOptions: {
208
+            parse: {
209
+              // We want terser to parse ecma 8 code. However, we don't want it
210
+              // to apply any minification steps that turns valid ecma 5 code
211
+              // into invalid ecma 5 code. This is why the 'compress' and 'output'
212
+              // sections only apply transformations that are ecma 5 safe
213
+              // https://github.com/facebook/create-react-app/pull/4234
214
+              ecma: 8,
215
+            },
216
+            compress: {
217
+              ecma: 5,
218
+              warnings: false,
219
+              // Disabled because of an issue with Uglify breaking seemingly valid code:
220
+              // https://github.com/facebook/create-react-app/issues/2376
221
+              // Pending further investigation:
222
+              // https://github.com/mishoo/UglifyJS2/issues/2011
223
+              comparisons: false,
224
+              // Disabled because of an issue with Terser breaking valid code:
225
+              // https://github.com/facebook/create-react-app/issues/5250
226
+              // Pending further investigation:
227
+              // https://github.com/terser-js/terser/issues/120
228
+              inline: 2,
229
+            },
230
+            mangle: {
231
+              safari10: true,
232
+            },
233
+            // Added for profiling in devtools
234
+            keep_classnames: isEnvProductionProfile,
235
+            keep_fnames: isEnvProductionProfile,
236
+            output: {
237
+              ecma: 5,
238
+              comments: false,
239
+              // Turned on because emoji and regex is not minified properly using default
240
+              // https://github.com/facebook/create-react-app/issues/2488
241
+              ascii_only: true,
242
+            },
243
+          },
244
+          sourceMap: shouldUseSourceMap,
245
+        }),
246
+        // This is only used in production mode
247
+        new OptimizeCSSAssetsPlugin({
248
+          cssProcessorOptions: {
249
+            parser: safePostCssParser,
250
+            map: shouldUseSourceMap
251
+              ? {
252
+                  // `inline: false` forces the sourcemap to be output into a
253
+                  // separate file
254
+                  inline: false,
255
+                  // `annotation: true` appends the sourceMappingURL to the end of
256
+                  // the css file, helping the browser find the sourcemap
257
+                  annotation: true,
258
+                }
259
+              : false,
260
+          },
261
+        }),
262
+      ],
263
+      // Automatically split vendor and commons
264
+      // https://twitter.com/wSokra/status/969633336732905474
265
+      // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
266
+      splitChunks: {
267
+        chunks: 'all',
268
+        name: false,
269
+      },
270
+      // Keep the runtime chunk separated to enable long term caching
271
+      // https://twitter.com/wSokra/status/969679223278505985
272
+      // https://github.com/facebook/create-react-app/issues/5358
273
+      runtimeChunk: {
274
+        name: entrypoint => `runtime-${entrypoint.name}`,
275
+      },
276
+    },
277
+    resolve: {
278
+      // This allows you to set a fallback for where Webpack should look for modules.
279
+      // We placed these paths second because we want `node_modules` to "win"
280
+      // if there are any conflicts. This matches Node resolution mechanism.
281
+      // https://github.com/facebook/create-react-app/issues/253
282
+      modules: ['node_modules', paths.appNodeModules].concat(
283
+        modules.additionalModulePaths || []
284
+      ),
285
+      // These are the reasonable defaults supported by the Node ecosystem.
286
+      // We also include JSX as a common component filename extension to support
287
+      // some tools, although we do not recommend using it, see:
288
+      // https://github.com/facebook/create-react-app/issues/290
289
+      // `web` extension prefixes have been added for better support
290
+      // for React Native Web.
291
+      extensions: paths.moduleFileExtensions
292
+        .map(ext => `.${ext}`)
293
+        .filter(ext => useTypeScript || !ext.includes('ts')),
294
+      alias: {
295
+        // Support React Native Web
296
+        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
297
+        'react-native': 'react-native-web',
298
+        // Allows for better profiling with ReactDevTools
299
+        ...(isEnvProductionProfile && {
300
+          'react-dom$': 'react-dom/profiling',
301
+          'scheduler/tracing': 'scheduler/tracing-profiling',
302
+        }),
303
+        ...(modules.webpackAliases || {}),
304
+      },
305
+      plugins: [
306
+        // Adds support for installing with Plug'n'Play, leading to faster installs and adding
307
+        // guards against forgotten dependencies and such.
308
+        PnpWebpackPlugin,
309
+        // Prevents users from importing files from outside of src/ (or node_modules/).
310
+        // This often causes confusion because we only process files within src/ with babel.
311
+        // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
312
+        // please link the files into your node_modules/ and let module-resolution kick in.
313
+        // Make sure your source files are compiled, as they will not be processed in any way.
314
+        new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
315
+      ],
316
+    },
317
+    resolveLoader: {
318
+      plugins: [
319
+        // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
320
+        // from the current package.
321
+        PnpWebpackPlugin.moduleLoader(module),
322
+      ],
323
+    },
324
+    module: {
325
+      strictExportPresence: true,
326
+      rules: [
327
+        // Disable require.ensure as it's not a standard language feature.
328
+        { parser: { requireEnsure: false } },
329
+
330
+        // First, run the linter.
331
+        // It's important to do this before Babel processes the JS.
332
+        {
333
+          test: /\.(js|mjs|jsx|ts|tsx)$/,
334
+          enforce: 'pre',
335
+          use: [
336
+            {
337
+              options: {
338
+                cache: true,
339
+                formatter: require.resolve('react-dev-utils/eslintFormatter'),
340
+                eslintPath: require.resolve('eslint'),
341
+                resolvePluginsRelativeTo: __dirname,
342
+                
343
+              },
344
+              loader: require.resolve('eslint-loader'),
345
+            },
346
+          ],
347
+          include: paths.appSrc,
348
+        },
349
+        {
350
+          // "oneOf" will traverse all following loaders until one will
351
+          // match the requirements. When no loader matches it will fall
352
+          // back to the "file" loader at the end of the loader list.
353
+          oneOf: [
354
+            // "url" loader works like "file" loader except that it embeds assets
355
+            // smaller than specified limit in bytes as data URLs to avoid requests.
356
+            // A missing `test` is equivalent to a match.
357
+            {
358
+              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
359
+              loader: require.resolve('url-loader'),
360
+              options: {
361
+                limit: imageInlineSizeLimit,
362
+                name: 'static/media/[name].[hash:8].[ext]',
363
+              },
364
+            },
365
+            // Process application JS with Babel.
366
+            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
367
+            {
368
+              test: /\.(js|mjs|jsx|ts|tsx)$/,
369
+              include: paths.appSrc,
370
+              loader: require.resolve('babel-loader'),
371
+              options: {
372
+                customize: require.resolve(
373
+                  'babel-preset-react-app/webpack-overrides'
374
+                ),
375
+                
376
+                plugins: [
377
+                  [
378
+                    require.resolve('babel-plugin-named-asset-import'),
379
+                    {
380
+                      loaderMap: {
381
+                        svg: {
382
+                          ReactComponent:
383
+                            '@svgr/webpack?-svgo,+titleProp,+ref![path]',
384
+                        },
385
+                      },
386
+                    },
387
+                  ],
388
+                ],
389
+                // This is a feature of `babel-loader` for webpack (not Babel itself).
390
+                // It enables caching results in ./node_modules/.cache/babel-loader/
391
+                // directory for faster rebuilds.
392
+                cacheDirectory: true,
393
+                // See #6846 for context on why cacheCompression is disabled
394
+                cacheCompression: false,
395
+                compact: isEnvProduction,
396
+              },
397
+            },
398
+            // Process any JS outside of the app with Babel.
399
+            // Unlike the application JS, we only compile the standard ES features.
400
+            {
401
+              test: /\.(js|mjs)$/,
402
+              exclude: /@babel(?:\/|\\{1,2})runtime/,
403
+              loader: require.resolve('babel-loader'),
404
+              options: {
405
+                babelrc: false,
406
+                configFile: false,
407
+                compact: false,
408
+                presets: [
409
+                  [
410
+                    require.resolve('babel-preset-react-app/dependencies'),
411
+                    { helpers: true },
412
+                  ],
413
+                ],
414
+                cacheDirectory: true,
415
+                // See #6846 for context on why cacheCompression is disabled
416
+                cacheCompression: false,
417
+                
418
+                // Babel sourcemaps are needed for debugging into node_modules
419
+                // code.  Without the options below, debuggers like VSCode
420
+                // show incorrect code and set breakpoints on the wrong lines.
421
+                sourceMaps: shouldUseSourceMap,
422
+                inputSourceMap: shouldUseSourceMap,
423
+              },
424
+            },
425
+            // "postcss" loader applies autoprefixer to our CSS.
426
+            // "css" loader resolves paths in CSS and adds assets as dependencies.
427
+            // "style" loader turns CSS into JS modules that inject <style> tags.
428
+            // In production, we use MiniCSSExtractPlugin to extract that CSS
429
+            // to a file, but in development "style" loader enables hot editing
430
+            // of CSS.
431
+            // By default we support CSS Modules with the extension .module.css
432
+            {
433
+              test: cssRegex,
434
+              exclude: cssModuleRegex,
435
+              use: getStyleLoaders({
436
+                importLoaders: 1,
437
+                sourceMap: isEnvProduction && shouldUseSourceMap,
438
+              }),
439
+              // Don't consider CSS imports dead code even if the
440
+              // containing package claims to have no side effects.
441
+              // Remove this when webpack adds a warning or an error for this.
442
+              // See https://github.com/webpack/webpack/issues/6571
443
+              sideEffects: true,
444
+            },
445
+            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
446
+            // using the extension .module.css
447
+            {
448
+              test: cssModuleRegex,
449
+              use: getStyleLoaders({
450
+                importLoaders: 1,
451
+                sourceMap: isEnvProduction && shouldUseSourceMap,
452
+                modules: {
453
+                  getLocalIdent: getCSSModuleLocalIdent,
454
+                },
455
+              }),
456
+            },
457
+            // Opt-in support for SASS (using .scss or .sass extensions).
458
+            // By default we support SASS Modules with the
459
+            // extensions .module.scss or .module.sass
460
+            {
461
+              test: sassRegex,
462
+              exclude: sassModuleRegex,
463
+              use: getStyleLoaders(
464
+                {
465
+                  importLoaders: 2,
466
+                  sourceMap: isEnvProduction && shouldUseSourceMap,
467
+                },
468
+                'sass-loader'
469
+              ),
470
+              // Don't consider CSS imports dead code even if the
471
+              // containing package claims to have no side effects.
472
+              // Remove this when webpack adds a warning or an error for this.
473
+              // See https://github.com/webpack/webpack/issues/6571
474
+              sideEffects: true,
475
+            },
476
+            // Adds support for CSS Modules, but using SASS
477
+            // using the extension .module.scss or .module.sass
478
+            {
479
+              test: sassModuleRegex,
480
+              use: getStyleLoaders(
481
+                {
482
+                  importLoaders: 2,
483
+                  sourceMap: isEnvProduction && shouldUseSourceMap,
484
+                  modules: {
485
+                    getLocalIdent: getCSSModuleLocalIdent,
486
+                  },
487
+                },
488
+                'sass-loader'
489
+              ),
490
+            },
491
+            // "file" loader makes sure those assets get served by WebpackDevServer.
492
+            // When you `import` an asset, you get its (virtual) filename.
493
+            // In production, they would get copied to the `build` folder.
494
+            // This loader doesn't use a "test" so it will catch all modules
495
+            // that fall through the other loaders.
496
+            {
497
+              loader: require.resolve('file-loader'),
498
+              // Exclude `js` files to keep "css" loader working as it injects
499
+              // its runtime that would otherwise be processed through "file" loader.
500
+              // Also exclude `html` and `json` extensions so they get processed
501
+              // by webpacks internal loaders.
502
+              exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
503
+              options: {
504
+                name: 'static/media/[name].[hash:8].[ext]',
505
+              },
506
+            },
507
+            // ** STOP ** Are you adding a new loader?
508
+            // Make sure to add the new loader(s) before the "file" loader.
509
+          ],
510
+        },
511
+      ],
512
+    },
513
+    plugins: [
514
+      // Generates an `index.html` file with the <script> injected.
515
+      new HtmlWebpackPlugin(
516
+        Object.assign(
517
+          {},
518
+          {
519
+            inject: true,
520
+            template: paths.appHtml,
521
+          },
522
+          isEnvProduction
523
+            ? {
524
+                minify: {
525
+                  removeComments: true,
526
+                  collapseWhitespace: true,
527
+                  removeRedundantAttributes: true,
528
+                  useShortDoctype: true,
529
+                  removeEmptyAttributes: true,
530
+                  removeStyleLinkTypeAttributes: true,
531
+                  keepClosingSlash: true,
532
+                  minifyJS: true,
533
+                  minifyCSS: true,
534
+                  minifyURLs: true,
535
+                },
536
+              }
537
+            : undefined
538
+        )
539
+      ),
540
+      // Inlines the webpack runtime script. This script is too small to warrant
541
+      // a network request.
542
+      // https://github.com/facebook/create-react-app/issues/5358
543
+      isEnvProduction &&
544
+        shouldInlineRuntimeChunk &&
545
+        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
546
+      // Makes some environment variables available in index.html.
547
+      // The public URL is available as %PUBLIC_URL% in index.html, e.g.:
548
+      // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
549
+      // In production, it will be an empty string unless you specify "homepage"
550
+      // in `package.json`, in which case it will be the pathname of that URL.
551
+      // In development, this will be an empty string.
552
+      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
553
+      // This gives some necessary context to module not found errors, such as
554
+      // the requesting resource.
555
+      new ModuleNotFoundPlugin(paths.appPath),
556
+      // Makes some environment variables available to the JS code, for example:
557
+      // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
558
+      // It is absolutely essential that NODE_ENV is set to production
559
+      // during a production build.
560
+      // Otherwise React will be compiled in the very slow development mode.
561
+      new webpack.DefinePlugin(env.stringified),
562
+      // This is necessary to emit hot updates (currently CSS only):
563
+      isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
564
+      // Watcher doesn't work well if you mistype casing in a path so we use
565
+      // a plugin that prints an error when you attempt to do this.
566
+      // See https://github.com/facebook/create-react-app/issues/240
567
+      isEnvDevelopment && new CaseSensitivePathsPlugin(),
568
+      // If you require a missing module and then `npm install` it, you still have
569
+      // to restart the development server for Webpack to discover it. This plugin
570
+      // makes the discovery automatic so you don't have to restart.
571
+      // See https://github.com/facebook/create-react-app/issues/186
572
+      isEnvDevelopment &&
573
+        new WatchMissingNodeModulesPlugin(paths.appNodeModules),
574
+      isEnvProduction &&
575
+        new MiniCssExtractPlugin({
576
+          // Options similar to the same options in webpackOptions.output
577
+          // both options are optional
578
+          filename: 'static/css/[name].[contenthash:8].css',
579
+          chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
580
+        }),
581
+      // Generate an asset manifest file with the following content:
582
+      // - "files" key: Mapping of all asset filenames to their corresponding
583
+      //   output file so that tools can pick it up without having to parse
584
+      //   `index.html`
585
+      // - "entrypoints" key: Array of files which are included in `index.html`,
586
+      //   can be used to reconstruct the HTML if necessary
587
+      new ManifestPlugin({
588
+        fileName: 'asset-manifest.json',
589
+        publicPath: publicPath,
590
+        generate: (seed, files, entrypoints) => {
591
+          const manifestFiles = files.reduce((manifest, file) => {
592
+            manifest[file.name] = file.path;
593
+            return manifest;
594
+          }, seed);
595
+          const entrypointFiles = entrypoints.main.filter(
596
+            fileName => !fileName.endsWith('.map')
597
+          );
598
+
599
+          return {
600
+            files: manifestFiles,
601
+            entrypoints: entrypointFiles,
602
+          };
603
+        },
604
+      }),
605
+      // Moment.js is an extremely popular library that bundles large locale files
606
+      // by default due to how Webpack interprets its code. This is a practical
607
+      // solution that requires the user to opt into importing specific locales.
608
+      // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
609
+      // You can remove this if you don't use Moment.js:
610
+      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
611
+      // Generate a service worker script that will precache, and keep up to date,
612
+      // the HTML & assets that are part of the Webpack build.
613
+      isEnvProduction &&
614
+        new WorkboxWebpackPlugin.GenerateSW({
615
+          clientsClaim: true,
616
+          exclude: [/\.map$/, /asset-manifest\.json$/],
617
+          importWorkboxFrom: 'cdn',
618
+          navigateFallback: publicUrl + '/index.html',
619
+          navigateFallbackBlacklist: [
620
+            // Exclude URLs starting with /_, as they're likely an API call
621
+            new RegExp('^/_'),
622
+            // Exclude any URLs whose last part seems to be a file extension
623
+            // as they're likely a resource and not a SPA route.
624
+            // URLs containing a "?" character won't be blacklisted as they're likely
625
+            // a route with query params (e.g. auth callbacks).
626
+            new RegExp('/[^/?]+\\.[^/]+$'),
627
+          ],
628
+        }),
629
+      // TypeScript type checking
630
+      useTypeScript &&
631
+        new ForkTsCheckerWebpackPlugin({
632
+          typescript: resolve.sync('typescript', {
633
+            basedir: paths.appNodeModules,
634
+          }),
635
+          async: isEnvDevelopment,
636
+          useTypescriptIncrementalApi: true,
637
+          checkSyntacticErrors: true,
638
+          resolveModuleNameModule: process.versions.pnp
639
+            ? `${__dirname}/pnpTs.js`
640
+            : undefined,
641
+          resolveTypeReferenceDirectiveModule: process.versions.pnp
642
+            ? `${__dirname}/pnpTs.js`
643
+            : undefined,
644
+          tsconfig: paths.appTsConfig,
645
+          reportFiles: [
646
+            '**',
647
+            '!**/__tests__/**',
648
+            '!**/?(*.)(spec|test).*',
649
+            '!**/src/setupProxy.*',
650
+            '!**/src/setupTests.*',
651
+          ],
652
+          silent: true,
653
+          // The formatter is invoked directly in WebpackDevServerUtils during development
654
+          formatter: isEnvProduction ? typescriptFormatter : undefined,
655
+        }),
656
+    ].filter(Boolean),
657
+    // Some libraries import Node modules but don't use them in the browser.
658
+    // Tell Webpack to provide empty mocks for them so importing them works.
659
+    node: {
660
+      module: 'empty',
661
+      dgram: 'empty',
662
+      dns: 'mock',
663
+      fs: 'empty',
664
+      http2: 'empty',
665
+      net: 'empty',
666
+      tls: 'empty',
667
+      child_process: 'empty',
668
+    },
669
+    // Turn off performance processing because we utilize
670
+    // our own hints via the FileSizeReporter
671
+    performance: false,
672
+  };
673
+};

+ 110
- 0
config/webpackDevServer.config.js View File

@@ -0,0 +1,110 @@
1
+'use strict';
2
+
3
+const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
4
+const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
5
+const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
6
+const ignoredFiles = require('react-dev-utils/ignoredFiles');
7
+const paths = require('./paths');
8
+const fs = require('fs');
9
+
10
+const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
11
+const host = process.env.HOST || '0.0.0.0';
12
+
13
+module.exports = function(proxy, allowedHost) {
14
+  return {
15
+    // WebpackDevServer 2.4.3 introduced a security fix that prevents remote
16
+    // websites from potentially accessing local content through DNS rebinding:
17
+    // https://github.com/webpack/webpack-dev-server/issues/887
18
+    // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
19
+    // However, it made several existing use cases such as development in cloud
20
+    // environment or subdomains in development significantly more complicated:
21
+    // https://github.com/facebook/create-react-app/issues/2271
22
+    // https://github.com/facebook/create-react-app/issues/2233
23
+    // While we're investigating better solutions, for now we will take a
24
+    // compromise. Since our WDS configuration only serves files in the `public`
25
+    // folder we won't consider accessing them a vulnerability. However, if you
26
+    // use the `proxy` feature, it gets more dangerous because it can expose
27
+    // remote code execution vulnerabilities in backends like Django and Rails.
28
+    // So we will disable the host check normally, but enable it if you have
29
+    // specified the `proxy` setting. Finally, we let you override it if you
30
+    // really know what you're doing with a special environment variable.
31
+    disableHostCheck:
32
+      !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
33
+    // Enable gzip compression of generated files.
34
+    compress: true,
35
+    // Silence WebpackDevServer's own logs since they're generally not useful.
36
+    // It will still show compile warnings and errors with this setting.
37
+    clientLogLevel: 'none',
38
+    // By default WebpackDevServer serves physical files from current directory
39
+    // in addition to all the virtual build products that it serves from memory.
40
+    // This is confusing because those files won’t automatically be available in
41
+    // production build folder unless we copy them. However, copying the whole
42
+    // project directory is dangerous because we may expose sensitive files.
43
+    // Instead, we establish a convention that only files in `public` directory
44
+    // get served. Our build script will copy `public` into the `build` folder.
45
+    // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
46
+    // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
47
+    // In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
48
+    // Note that we only recommend to use `public` folder as an escape hatch
49
+    // for files like `favicon.ico`, `manifest.json`, and libraries that are
50
+    // for some reason broken when imported through Webpack. If you just want to
51
+    // use an image, put it in `src` and `import` it from JavaScript instead.
52
+    contentBase: paths.appPublic,
53
+    // By default files from `contentBase` will not trigger a page reload.
54
+    watchContentBase: true,
55
+    // Enable hot reloading server. It will provide /sockjs-node/ endpoint
56
+    // for the WebpackDevServer client so it can learn when the files were
57
+    // updated. The WebpackDevServer client is included as an entry point
58
+    // in the Webpack development configuration. Note that only changes
59
+    // to CSS are currently hot reloaded. JS changes will refresh the browser.
60
+    hot: true,
61
+    // Use 'ws' instead of 'sockjs-node' on server since we're using native
62
+    // websockets in `webpackHotDevClient`.
63
+    transportMode: 'ws',
64
+    // Prevent a WS client from getting injected as we're already including
65
+    // `webpackHotDevClient`.
66
+    injectClient: false,
67
+    // It is important to tell WebpackDevServer to use the same "root" path
68
+    // as we specified in the config. In development, we always serve from /.
69
+    publicPath: '/',
70
+    // WebpackDevServer is noisy by default so we emit custom message instead
71
+    // by listening to the compiler events with `compiler.hooks[...].tap` calls above.
72
+    quiet: true,
73
+    // Reportedly, this avoids CPU overload on some systems.
74
+    // https://github.com/facebook/create-react-app/issues/293
75
+    // src/node_modules is not ignored to support absolute imports
76
+    // https://github.com/facebook/create-react-app/issues/1065
77
+    watchOptions: {
78
+      ignored: ignoredFiles(paths.appSrc),
79
+    },
80
+    // Enable HTTPS if the HTTPS environment variable is set to 'true'
81
+    https: protocol === 'https',
82
+    host,
83
+    overlay: false,
84
+    historyApiFallback: {
85
+      // Paths with dots should still use the history fallback.
86
+      // See https://github.com/facebook/create-react-app/issues/387.
87
+      disableDotRule: true,
88
+    },
89
+    public: allowedHost,
90
+    proxy,
91
+    before(app, server) {
92
+      if (fs.existsSync(paths.proxySetup)) {
93
+        // This registers user provided middleware for proxy reasons
94
+        require(paths.proxySetup)(app);
95
+      }
96
+
97
+      // This lets us fetch source contents from webpack for the error overlay
98
+      app.use(evalSourceMapMiddleware(server));
99
+      // This lets us open files from the runtime error overlay.
100
+      app.use(errorOverlayMiddleware());
101
+
102
+      // This service worker file is effectively a 'no-op' that will reset any
103
+      // previous service worker registered for the same host:port combination.
104
+      // We do this in development to avoid hitting the production cache if
105
+      // it used the same host and port.
106
+      // https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
107
+      app.use(noopServiceWorkerMiddleware());
108
+    },
109
+  };
110
+};

+ 117
- 10
package.json View File

@@ -1,20 +1,72 @@
1 1
 {
2 2
   "name": "videoplayer",
3 3
   "version": "0.1.0",
4
-  "private": true,
4
+  "scripts": {
5
+    "start": "react-scripts start",
6
+    "build": "rreact-scripts build",
7
+    "test": "react-scripts test"
8
+  },
5 9
   "dependencies": {
6
-    "@testing-library/jest-dom": "^4.2.4",
7
-    "@testing-library/react": "^9.3.2",
8
-    "@testing-library/user-event": "^7.1.2",
10
+    "@babel/core": "7.7.4",
11
+    "@svgr/webpack": "4.3.3",
12
+    "@typescript-eslint/eslint-plugin": "^2.8.0",
13
+    "@typescript-eslint/parser": "^2.8.0",
14
+    "babel-eslint": "10.0.3",
15
+    "babel-jest": "^24.9.0",
16
+    "babel-loader": "8.0.6",
17
+    "babel-plugin-named-asset-import": "^0.3.5",
18
+    "babel-preset-react-app": "^9.1.0",
19
+    "camelcase": "^5.3.1",
20
+    "case-sensitive-paths-webpack-plugin": "2.2.0",
21
+    "css-loader": "3.2.0",
22
+    "dotenv": "8.2.0",
23
+    "dotenv-expand": "5.1.0",
24
+    "eslint": "^6.6.0",
25
+    "eslint-config-react-app": "^5.1.0",
26
+    "eslint-loader": "3.0.2",
27
+    "eslint-plugin-flowtype": "3.13.0",
28
+    "eslint-plugin-import": "2.18.2",
29
+    "eslint-plugin-jsx-a11y": "6.2.3",
30
+    "eslint-plugin-react": "7.16.0",
31
+    "eslint-plugin-react-hooks": "^1.6.1",
32
+    "file-loader": "4.3.0",
33
+    "fs-extra": "^8.1.0",
34
+    "html-webpack-plugin": "4.0.0-beta.5",
35
+    "identity-obj-proxy": "3.0.0",
36
+    "jest": "24.9.0",
37
+    "jest-environment-jsdom-fourteen": "0.1.0",
38
+    "jest-resolve": "24.9.0",
39
+    "jest-watch-typeahead": "0.4.2",
40
+    "mini-css-extract-plugin": "0.8.0",
41
+    "optimize-css-assets-webpack-plugin": "5.0.3",
42
+    "pnp-webpack-plugin": "1.5.0",
43
+    "postcss-flexbugs-fixes": "4.1.0",
44
+    "postcss-loader": "3.0.0",
45
+    "postcss-normalize": "8.0.1",
46
+    "postcss-preset-env": "6.7.0",
47
+    "postcss-safe-parser": "4.0.1",
9 48
     "react": "^16.12.0",
49
+    "react-app-polyfill": "^1.0.5",
50
+    "react-dev-utils": "^10.0.0",
10 51
     "react-dom": "^16.12.0",
11
-    "react-scripts": "3.3.0"
52
+    "resolve": "1.12.2",
53
+    "resolve-url-loader": "3.1.1",
54
+    "sass-loader": "8.0.0",
55
+    "semver": "6.3.0",
56
+    "style-loader": "1.0.0",
57
+    "terser-webpack-plugin": "2.2.1",
58
+    "ts-pnp": "1.1.5",
59
+    "url-loader": "2.3.0",
60
+    "webpack": "4.41.2",
61
+    "webpack-dev-server": "3.9.0",
62
+    "webpack-manifest-plugin": "2.2.0",
63
+    "workbox-webpack-plugin": "4.3.1"
12 64
   },
13
-  "scripts": {
14
-    "start": "react-scripts start",
15
-    "build": "react-scripts build",
16
-    "test": "react-scripts test",
17
-    "eject": "react-scripts eject"
65
+  "devDependencies": {
66
+    "@testing-library/jest-dom": "^4.2.4",
67
+    "@testing-library/react": "^9.3.2",
68
+    "@testing-library/user-event": "^7.1.2",
69
+    "react-scripts": "^3.3.0"
18 70
   },
19 71
   "eslintConfig": {
20 72
     "extends": "react-app"
@@ -30,5 +82,60 @@
30 82
       "last 1 firefox version",
31 83
       "last 1 safari version"
32 84
     ]
85
+  },
86
+  "jest": {
87
+    "roots": [
88
+      "<rootDir>/src"
89
+    ],
90
+    "collectCoverageFrom": [
91
+      "src/**/*.{js,jsx,ts,tsx}",
92
+      "!src/**/*.d.ts"
93
+    ],
94
+    "setupFiles": [
95
+      "react-app-polyfill/jsdom"
96
+    ],
97
+    "setupFilesAfterEnv": [
98
+      "<rootDir>/src/setupTests.js"
99
+    ],
100
+    "testMatch": [
101
+      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
102
+      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
103
+    ],
104
+    "testEnvironment": "jest-environment-jsdom-fourteen",
105
+    "transform": {
106
+      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
107
+      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
108
+      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
109
+    },
110
+    "transformIgnorePatterns": [
111
+      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
112
+      "^.+\\.module\\.(css|sass|scss)$"
113
+    ],
114
+    "modulePaths": [],
115
+    "moduleNameMapper": {
116
+      "^react-native$": "react-native-web",
117
+      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
118
+    },
119
+    "moduleFileExtensions": [
120
+      "web.js",
121
+      "js",
122
+      "web.ts",
123
+      "ts",
124
+      "web.tsx",
125
+      "tsx",
126
+      "json",
127
+      "web.jsx",
128
+      "jsx",
129
+      "node"
130
+    ],
131
+    "watchPlugins": [
132
+      "jest-watch-typeahead/filename",
133
+      "jest-watch-typeahead/testname"
134
+    ]
135
+  },
136
+  "babel": {
137
+    "presets": [
138
+      "react-app"
139
+    ]
33 140
   }
34 141
 }

+ 211
- 0
scripts/build.js View File

@@ -0,0 +1,211 @@
1
+'use strict';
2
+
3
+// Do this as the first thing so that any code reading it knows the right env.
4
+process.env.BABEL_ENV = 'production';
5
+process.env.NODE_ENV = 'production';
6
+
7
+// Makes the script crash on unhandled rejections instead of silently
8
+// ignoring them. In the future, promise rejections that are not handled will
9
+// terminate the Node.js process with a non-zero exit code.
10
+process.on('unhandledRejection', err => {
11
+  throw err;
12
+});
13
+
14
+// Ensure environment variables are read.
15
+require('../config/env');
16
+
17
+
18
+const path = require('path');
19
+const chalk = require('react-dev-utils/chalk');
20
+const fs = require('fs-extra');
21
+const webpack = require('webpack');
22
+const configFactory = require('../config/webpack.config');
23
+const paths = require('../config/paths');
24
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
25
+const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
26
+const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
27
+const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
28
+const printBuildError = require('react-dev-utils/printBuildError');
29
+
30
+const measureFileSizesBeforeBuild =
31
+  FileSizeReporter.measureFileSizesBeforeBuild;
32
+const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
33
+const useYarn = fs.existsSync(paths.yarnLockFile);
34
+
35
+// These sizes are pretty large. We'll warn for bundles exceeding them.
36
+const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
37
+const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
38
+
39
+const isInteractive = process.stdout.isTTY;
40
+
41
+// Warn and crash if required files are missing
42
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
43
+  process.exit(1);
44
+}
45
+
46
+// Generate configuration
47
+const config = configFactory('production');
48
+
49
+// We require that you explicitly set browsers and do not fall back to
50
+// browserslist defaults.
51
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
52
+checkBrowsers(paths.appPath, isInteractive)
53
+  .then(() => {
54
+    // First, read the current file sizes in build directory.
55
+    // This lets us display how much they changed later.
56
+    return measureFileSizesBeforeBuild(paths.appBuild);
57
+  })
58
+  .then(previousFileSizes => {
59
+    // Remove all content but keep the directory so that
60
+    // if you're in it, you don't end up in Trash
61
+    fs.emptyDirSync(paths.appBuild);
62
+    // Merge with the public folder
63
+    copyPublicFolder();
64
+    // Start the webpack build
65
+    return build(previousFileSizes);
66
+  })
67
+  .then(
68
+    ({ stats, previousFileSizes, warnings }) => {
69
+      if (warnings.length) {
70
+        console.log(chalk.yellow('Compiled with warnings.\n'));
71
+        console.log(warnings.join('\n\n'));
72
+        console.log(
73
+          '\nSearch for the ' +
74
+            chalk.underline(chalk.yellow('keywords')) +
75
+            ' to learn more about each warning.'
76
+        );
77
+        console.log(
78
+          'To ignore, add ' +
79
+            chalk.cyan('// eslint-disable-next-line') +
80
+            ' to the line before.\n'
81
+        );
82
+      } else {
83
+        console.log(chalk.green('Compiled successfully.\n'));
84
+      }
85
+
86
+      console.log('File sizes after gzip:\n');
87
+      printFileSizesAfterBuild(
88
+        stats,
89
+        previousFileSizes,
90
+        paths.appBuild,
91
+        WARN_AFTER_BUNDLE_GZIP_SIZE,
92
+        WARN_AFTER_CHUNK_GZIP_SIZE
93
+      );
94
+      console.log();
95
+
96
+      const appPackage = require(paths.appPackageJson);
97
+      const publicUrl = paths.publicUrl;
98
+      const publicPath = config.output.publicPath;
99
+      const buildFolder = path.relative(process.cwd(), paths.appBuild);
100
+      printHostingInstructions(
101
+        appPackage,
102
+        publicUrl,
103
+        publicPath,
104
+        buildFolder,
105
+        useYarn
106
+      );
107
+    },
108
+    err => {
109
+      const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
110
+      if (tscCompileOnError) {
111
+        console.log(
112
+          chalk.yellow(
113
+            'Compiled with the following type errors (you may want to check these before deploying your app):\n'
114
+          )
115
+        );
116
+        printBuildError(err);
117
+      } else {
118
+        console.log(chalk.red('Failed to compile.\n'));
119
+        printBuildError(err);
120
+        process.exit(1);
121
+      }
122
+    }
123
+  )
124
+  .catch(err => {
125
+    if (err && err.message) {
126
+      console.log(err.message);
127
+    }
128
+    process.exit(1);
129
+  });
130
+
131
+// Create the production build and print the deployment instructions.
132
+function build(previousFileSizes) {
133
+  // We used to support resolving modules according to `NODE_PATH`.
134
+  // This now has been deprecated in favor of jsconfig/tsconfig.json
135
+  // This lets you use absolute paths in imports inside large monorepos:
136
+  if (process.env.NODE_PATH) {
137
+    console.log(
138
+      chalk.yellow(
139
+        'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
140
+      )
141
+    );
142
+    console.log();
143
+  }
144
+
145
+  console.log('Creating an optimized production build...');
146
+
147
+  const compiler = webpack(config);
148
+  return new Promise((resolve, reject) => {
149
+    compiler.run((err, stats) => {
150
+      let messages;
151
+      if (err) {
152
+        if (!err.message) {
153
+          return reject(err);
154
+        }
155
+
156
+        let errMessage = err.message;
157
+
158
+        // Add additional information for postcss errors
159
+        if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
160
+          errMessage +=
161
+            '\nCompileError: Begins at CSS selector ' +
162
+            err['postcssNode'].selector;
163
+        }
164
+
165
+        messages = formatWebpackMessages({
166
+          errors: [errMessage],
167
+          warnings: [],
168
+        });
169
+      } else {
170
+        messages = formatWebpackMessages(
171
+          stats.toJson({ all: false, warnings: true, errors: true })
172
+        );
173
+      }
174
+      if (messages.errors.length) {
175
+        // Only keep the first error. Others are often indicative
176
+        // of the same problem, but confuse the reader with noise.
177
+        if (messages.errors.length > 1) {
178
+          messages.errors.length = 1;
179
+        }
180
+        return reject(new Error(messages.errors.join('\n\n')));
181
+      }
182
+      if (
183
+        process.env.CI &&
184
+        (typeof process.env.CI !== 'string' ||
185
+          process.env.CI.toLowerCase() !== 'false') &&
186
+        messages.warnings.length
187
+      ) {
188
+        console.log(
189
+          chalk.yellow(
190
+            '\nTreating warnings as errors because process.env.CI = true.\n' +
191
+              'Most CI servers set it automatically.\n'
192
+          )
193
+        );
194
+        return reject(new Error(messages.warnings.join('\n\n')));
195
+      }
196
+
197
+      return resolve({
198
+        stats,
199
+        previousFileSizes,
200
+        warnings: messages.warnings,
201
+      });
202
+    });
203
+  });
204
+}
205
+
206
+function copyPublicFolder() {
207
+  fs.copySync(paths.appPublic, paths.appBuild, {
208
+    dereference: true,
209
+    filter: file => file !== paths.appHtml,
210
+  });
211
+}

+ 147
- 0
scripts/start.js View File

@@ -0,0 +1,147 @@
1
+'use strict';
2
+
3
+// Do this as the first thing so that any code reading it knows the right env.
4
+process.env.BABEL_ENV = 'development';
5
+process.env.NODE_ENV = 'development';
6
+
7
+// Makes the script crash on unhandled rejections instead of silently
8
+// ignoring them. In the future, promise rejections that are not handled will
9
+// terminate the Node.js process with a non-zero exit code.
10
+process.on('unhandledRejection', err => {
11
+  throw err;
12
+});
13
+
14
+// Ensure environment variables are read.
15
+require('../config/env');
16
+
17
+
18
+const fs = require('fs');
19
+const chalk = require('react-dev-utils/chalk');
20
+const webpack = require('webpack');
21
+const WebpackDevServer = require('webpack-dev-server');
22
+const clearConsole = require('react-dev-utils/clearConsole');
23
+const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
24
+const {
25
+  choosePort,
26
+  createCompiler,
27
+  prepareProxy,
28
+  prepareUrls,
29
+} = require('react-dev-utils/WebpackDevServerUtils');
30
+const openBrowser = require('react-dev-utils/openBrowser');
31
+const paths = require('../config/paths');
32
+const configFactory = require('../config/webpack.config');
33
+const createDevServerConfig = require('../config/webpackDevServer.config');
34
+
35
+const useYarn = fs.existsSync(paths.yarnLockFile);
36
+const isInteractive = process.stdout.isTTY;
37
+
38
+// Warn and crash if required files are missing
39
+if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
40
+  process.exit(1);
41
+}
42
+
43
+// Tools like Cloud9 rely on this.
44
+const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
45
+const HOST = process.env.HOST || '0.0.0.0';
46
+
47
+if (process.env.HOST) {
48
+  console.log(
49
+    chalk.cyan(
50
+      `Attempting to bind to HOST environment variable: ${chalk.yellow(
51
+        chalk.bold(process.env.HOST)
52
+      )}`
53
+    )
54
+  );
55
+  console.log(
56
+    `If this was unintentional, check that you haven't mistakenly set it in your shell.`
57
+  );
58
+  console.log(
59
+    `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
60
+  );
61
+  console.log();
62
+}
63
+
64
+// We require that you explicitly set browsers and do not fall back to
65
+// browserslist defaults.
66
+const { checkBrowsers } = require('react-dev-utils/browsersHelper');
67
+checkBrowsers(paths.appPath, isInteractive)
68
+  .then(() => {
69
+    // We attempt to use the default port but if it is busy, we offer the user to
70
+    // run on a different port. `choosePort()` Promise resolves to the next free port.
71
+    return choosePort(HOST, DEFAULT_PORT);
72
+  })
73
+  .then(port => {
74
+    if (port == null) {
75
+      // We have not found a port.
76
+      return;
77
+    }
78
+    const config = configFactory('development');
79
+    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
80
+    const appName = require(paths.appPackageJson).name;
81
+    const useTypeScript = fs.existsSync(paths.appTsConfig);
82
+    const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
83
+    const urls = prepareUrls(protocol, HOST, port);
84
+    const devSocket = {
85
+      warnings: warnings =>
86
+        devServer.sockWrite(devServer.sockets, 'warnings', warnings),
87
+      errors: errors =>
88
+        devServer.sockWrite(devServer.sockets, 'errors', errors),
89
+    };
90
+    // Create a webpack compiler that is configured with custom messages.
91
+    const compiler = createCompiler({
92
+      appName,
93
+      config,
94
+      devSocket,
95
+      urls,
96
+      useYarn,
97
+      useTypeScript,
98
+      tscCompileOnError,
99
+      webpack,
100
+    });
101
+    // Load proxy config
102
+    const proxySetting = require(paths.appPackageJson).proxy;
103
+    const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
104
+    // Serve webpack assets generated by the compiler over a web server.
105
+    const serverConfig = createDevServerConfig(
106
+      proxyConfig,
107
+      urls.lanUrlForConfig
108
+    );
109
+    const devServer = new WebpackDevServer(compiler, serverConfig);
110
+    // Launch WebpackDevServer.
111
+    devServer.listen(port, HOST, err => {
112
+      if (err) {
113
+        return console.log(err);
114
+      }
115
+      if (isInteractive) {
116
+        clearConsole();
117
+      }
118
+
119
+      // We used to support resolving modules according to `NODE_PATH`.
120
+      // This now has been deprecated in favor of jsconfig/tsconfig.json
121
+      // This lets you use absolute paths in imports inside large monorepos:
122
+      if (process.env.NODE_PATH) {
123
+        console.log(
124
+          chalk.yellow(
125
+            'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
126
+          )
127
+        );
128
+        console.log();
129
+      }
130
+
131
+      console.log(chalk.cyan('Starting the development server...\n'));
132
+      openBrowser(urls.localUrlForBrowser);
133
+    });
134
+
135
+    ['SIGINT', 'SIGTERM'].forEach(function(sig) {
136
+      process.on(sig, function() {
137
+        devServer.close();
138
+        process.exit();
139
+      });
140
+    });
141
+  })
142
+  .catch(err => {
143
+    if (err && err.message) {
144
+      console.log(err.message);
145
+    }
146
+    process.exit(1);
147
+  });

+ 53
- 0
scripts/test.js View File

@@ -0,0 +1,53 @@
1
+'use strict';
2
+
3
+// Do this as the first thing so that any code reading it knows the right env.
4
+process.env.BABEL_ENV = 'test';
5
+process.env.NODE_ENV = 'test';
6
+process.env.PUBLIC_URL = '';
7
+
8
+// Makes the script crash on unhandled rejections instead of silently
9
+// ignoring them. In the future, promise rejections that are not handled will
10
+// terminate the Node.js process with a non-zero exit code.
11
+process.on('unhandledRejection', err => {
12
+  throw err;
13
+});
14
+
15
+// Ensure environment variables are read.
16
+require('../config/env');
17
+
18
+
19
+const jest = require('jest');
20
+const execSync = require('child_process').execSync;
21
+let argv = process.argv.slice(2);
22
+
23
+function isInGitRepository() {
24
+  try {
25
+    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
26
+    return true;
27
+  } catch (e) {
28
+    return false;
29
+  }
30
+}
31
+
32
+function isInMercurialRepository() {
33
+  try {
34
+    execSync('hg --cwd . root', { stdio: 'ignore' });
35
+    return true;
36
+  } catch (e) {
37
+    return false;
38
+  }
39
+}
40
+
41
+// Watch unless on CI or explicitly running all tests
42
+if (
43
+  !process.env.CI &&
44
+  argv.indexOf('--watchAll') === -1 &&
45
+  argv.indexOf('--watchAll=false') === -1
46
+) {
47
+  // https://github.com/facebook/create-react-app/issues/5210
48
+  const hasSourceControl = isInGitRepository() || isInMercurialRepository();
49
+  argv.push(hasSourceControl ? '--watch' : '--watchAll');
50
+}
51
+
52
+
53
+jest.run(argv);

+ 17
- 29
src/App.css View File

@@ -1,38 +1,26 @@
1
-.App {
2
-  text-align: center;
1
+.component {
2
+    padding: 20px;
3
+    font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
3 4
 }
4 5
 
5
-.App-logo {
6
-  height: 40vmin;
7
-  pointer-events: none;
6
+.header {
7
+    margin-bottom: 20px;
8 8
 }
9 9
 
10
-@media (prefers-reduced-motion: no-preference) {
11
-  .App-logo {
12
-    animation: App-logo-spin infinite 20s linear;
13
-  }
10
+.title {
11
+    font-size: 30px;
12
+    margin-bottom: 5px;
14 13
 }
15 14
 
16
-.App-header {
17
-  background-color: #282c34;
18
-  min-height: 100vh;
19
-  display: flex;
20
-  flex-direction: column;
21
-  align-items: center;
22
-  justify-content: center;
23
-  font-size: calc(10px + 2vmin);
24
-  color: white;
15
+.link {
16
+    color: #2492a8;
25 17
 }
26 18
 
27
-.App-link {
28
-  color: #61dafb;
29
-}
30
-
31
-@keyframes App-logo-spin {
32
-  from {
33
-    transform: rotate(0deg);
34
-  }
35
-  to {
36
-    transform: rotate(360deg);
37
-  }
19
+.videoListItem {
20
+    background-color: #efefef;
21
+    padding: 20px;
22
+    margin-bottom: 20px;
23
+    max-width: 634px;
24
+    max-height: 356px;
25
+    margin: 0 auto;
38 26
 }

+ 54
- 21
src/App.js View File

@@ -1,26 +1,59 @@
1
-import React from 'react';
2
-import logo from './logo.svg';
3
-import './App.css';
1
+import React, { Component } from "react";
2
+import { DefaultPlayer as Video } from "./components/entry";
4 3
 
5
-function App() {
6
-  return (
7
-    <div className="App">
8
-      <header className="App-header">
9
-        <img src={logo} className="App-logo" alt="logo" />
10
-        <p>
11
-          Edit <code>src/App.js</code> and save to reload.
12
-        </p>
13
-        <a
14
-          className="App-link"
15
-          href="https://reactjs.org"
16
-          target="_blank"
17
-          rel="noopener noreferrer"
4
+import poster from "./assets/1.png";
5
+// const sintelTrailer =
6
+//   "https://download.blender.org/durian/trailer/sintel_trailer-720p.mp4";
7
+const bigBuckBunny =
8
+  "http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_h264.mov";
9
+
10
+class App extends Component {
11
+  render() {
12
+    return (
13
+      <div>
14
+        <Video
15
+          ref="video2"
16
+          style={{ width: "100%", height: 500 }}
17
+          poster={poster}
18
+          data-playbackrates={JSON.stringify([
19
+            {
20
+              id: 0.5,
21
+              name: "0.5x",
22
+              mode: "disabled"
23
+            },
24
+            {
25
+              id: 0.75,
26
+              name: "0.75x",
27
+              mode: "disabled"
28
+            },
29
+            {
30
+              id: 1,
31
+              name: "Normal",
32
+              mode: "showing"
33
+            },
34
+            {
35
+              id: 1.25,
36
+              name: "1.25x",
37
+              mode: "disabled"
38
+            },
39
+            {
40
+              id: 1.5,
41
+              name: "1.5x",
42
+              mode: "disabled"
43
+            },
44
+            {
45
+              id: 2,
46
+              name: "2x",
47
+              mode: "disabled"
48
+            }
49
+          ])}
50
+          //   poster={bigBuckBunnyPoster}
18 51
         >
19
-          Learn React
20
-        </a>
21
-      </header>
22
-    </div>
23
-  );
52
+          <source src={require("./assets/1.mp4")} type="video/mp4" />
53
+        </Video>
54
+      </div>
55
+    );
56
+  }
24 57
 }
25 58
 
26 59
 export default App;

BIN
src/assets/1.mp4 View File


BIN
src/assets/1.png View File


+ 35
- 0
src/components/DefaultPlayer/Captions/Captions.js View File

@@ -0,0 +1,35 @@
1
+import React from "react";
2
+import styles from "./Captions.module.css";
3
+import { ReactComponent as ClosedCaptionIcon } from "../Icon/caption_new.svg";
4
+
5
+export default ({ textTracks, onClick, onItemClick, className, ariaLabel }) => {
6
+  // console.log('caption')
7
+  return (
8
+    <div className={[styles.component, className].join(" ")}>
9
+      <button
10
+        type="button"
11
+        onClick={onClick}
12
+        aria-label={ariaLabel}
13
+        className={styles.button}
14
+      >
15
+        <ClosedCaptionIcon className={styles.icon} fill="#fff" />
16
+      </button>
17
+      <ul className={styles.trackList}>
18
+        {textTracks &&
19
+          [...textTracks].map(track => (
20
+            <li
21
+              key={track.language}
22
+              className={
23
+                track.mode === track.SHOWING || track.mode === "showing"
24
+                  ? styles.activeTrackItem
25
+                  : styles.trackItem
26
+              }
27
+              onClick={onItemClick.bind(this, track)}
28
+            >
29
+              {track.label}
30
+            </li>
31
+          ))}
32
+      </ul>
33
+    </div>
34
+  );
35
+};

+ 56
- 0
src/components/DefaultPlayer/Captions/Captions.module.css View File

@@ -0,0 +1,56 @@
1
+.component:hover {
2
+    background-color: #000;
3
+}
4
+
5
+.button {
6
+    width: 34px;
7
+    height: 34px;
8
+    background: none;
9
+    border: 0;
10
+    color: inherit;
11
+    font: inherit;
12
+    line-height: normal;
13
+    overflow: visible;
14
+    padding: 0;
15
+    cursor: pointer;
16
+}
17
+
18
+.button:focus {
19
+    outline: 0;
20
+}
21
+
22
+.icon {
23
+    width: 24px;
24
+    height: 24px;
25
+}
26
+
27
+.trackList {
28
+    position: absolute;
29
+    right: 0;
30
+    bottom: 100%;
31
+    display: none;
32
+    background-color: rgba(0, 0, 0, 0.7);
33
+    list-style: none;
34
+    padding: 0;
35
+    margin: 0;
36
+    color: #fff;
37
+}
38
+
39
+.component:hover .trackList {
40
+    display: block;
41
+}
42
+
43
+.trackItem {
44
+    padding: 7px;
45
+    cursor: pointer;
46
+}
47
+
48
+.activeTrackItem,
49
+.trackItem:hover {
50
+    background: #000;
51
+}
52
+
53
+.activeTrackItem {
54
+    composes: trackItem;
55
+    text-decoration: underline;
56
+}

+ 32
- 0
src/components/DefaultPlayer/Captions/Captions.test.js View File

@@ -0,0 +1,32 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Captions from './Captions';
4
+import styles from './Captions.css';
5
+
6
+describe('Captions', () => {
7
+    let component;
8
+
9
+    beforeAll(() => {
10
+        component = shallow(
11
+            <Captions ariaLabel="Captions" />
12
+        );
13
+    });
14
+
15
+    it('should accept a className prop and append it to the components class', () => {
16
+        const newClassNameString = 'a new className';
17
+        expect(component.prop('className'))
18
+            .toContain(styles.component);
19
+        component.setProps({
20
+            className: newClassNameString
21
+        });
22
+        expect(component.prop('className'))
23
+            .toContain(styles.component);
24
+        expect(component.prop('className'))
25
+            .toContain(newClassNameString);
26
+    });
27
+
28
+    it('has correct aria-label', () => {
29
+        expect(component.find('button').prop('aria-label'))
30
+            .toEqual('Captions');
31
+    });
32
+});

+ 200
- 0
src/components/DefaultPlayer/DefaultPlayer.js View File

@@ -0,0 +1,200 @@
1
+import React from "react";
2
+import PropTypes from "prop-types";
3
+import videoConnect from "./../video/video";
4
+import copy from "./copy";
5
+import {
6
+  setVolume,
7
+  showTrack,
8
+  showSpeed,
9
+  toggleTracks,
10
+  toggleSpeeds,
11
+  toggleMute,
12
+  togglePause,
13
+  setCurrentTime,
14
+  toggleFullscreen,
15
+  getPercentagePlayed,
16
+  getPercentageBuffered
17
+} from "./../video/api";
18
+import styles from "./DefaultPlayer.module.css";
19
+import Time from "./Time/Time";
20
+import Seek from "./Seek/Seek";
21
+import Volume from "./Volume/Volume";
22
+import Captions from "./Captions/Captions";
23
+import Speed from "./Speed/Speed";
24
+import PlayPause from "./PlayPause/PlayPause";
25
+import Fullscreen from "./Fullscreen/Fullscreen";
26
+import Overlay from "./Overlay/Overlay";
27
+
28
+const DefaultPlayer = ({
29
+  copy,
30
+  video,
31
+  style,
32
+  controls,
33
+  children,
34
+  className,
35
+  onSeekChange,
36
+  onVolumeChange,
37
+  onVolumeClick,
38
+  onCaptionsClick,
39
+  onSpeedClick,
40
+  onPlayPauseClick,
41
+  onFullscreenClick,
42
+  onCaptionsItemClick,
43
+  onSpeedsItemClick,
44
+  ...restProps
45
+}) => {
46
+  let playbackrates = restProps["data-playbackrates"];
47
+  if (playbackrates) {
48
+    playbackrates = JSON.parse(playbackrates);
49
+  }
50
+  let onScreenClickCallback = restProps["onScreenClickCallback"];
51
+  return (
52
+    <div className={[styles.component, className].join(" ")} style={style}>
53
+      <video className={styles.video} {...restProps}>
54
+        {children}
55
+      </video>
56
+      <Overlay onClick={onPlayPauseClick} {...video} />
57
+      {controls && controls.length && !video.error && (
58
+        <div className={styles.controls}>
59
+          {controls.map((control, i) => {
60
+            switch (control) {
61
+              case "PlayPause":
62
+                return (
63
+                  <PlayPause
64
+                    key={i}
65
+                    ariaLabelPlay={copy.play}
66
+                    ariaLabelPause={copy.pause}
67
+                    onClick={onPlayPauseClick}
68
+                    {...video}
69
+                  />
70
+                );
71
+              case "Seek":
72
+                return (
73
+                  <Seek
74
+                    key={i}
75
+                    ariaLabel={copy.seek}
76
+                    className={styles.seek}
77
+                    onChange={onSeekChange}
78
+                    {...video}
79
+                  />
80
+                );
81
+              case "Time":
82
+                return <Time key={i} {...video} />;
83
+              case "Speed":
84
+                return playbackrates && playbackrates.length > 0 ? (
85
+                  <Speed
86
+                    key={i}
87
+                    onClick={onSpeedClick}
88
+                    ariaLabel={copy.captions}
89
+                    onItemClick={onSpeedsItemClick}
90
+                    playbackrates={playbackrates}
91
+                    {...video}
92
+                  />
93
+                ) : null;
94
+
95
+              case "Volume":
96
+                return (
97
+                  <Volume
98
+                    key={i}
99
+                    onClick={onVolumeClick}
100
+                    onChange={onVolumeChange}
101
+                    ariaLabelMute={copy.mute}
102
+                    ariaLabelUnmute={copy.unmute}
103
+                    ariaLabelVolume={copy.volume}
104
+                    {...video}
105
+                  />
106
+                );
107
+              case "Captions":
108
+                return video.textTracks && video.textTracks.length ? (
109
+                  <Captions
110
+                    key={i}
111
+                    onClick={onCaptionsClick}
112
+                    ariaLabel={copy.captions}
113
+                    onItemClick={onCaptionsItemClick}
114
+                    {...video}
115
+                  />
116
+                ) : null;
117
+              case "Fullscreen":
118
+                return (
119
+                  <Fullscreen
120
+                    key={i}
121
+                    ariaLabel={copy.fullscreen}
122
+                    onClick={onFullscreenClick}
123
+                    onScreenClickCallback={onScreenClickCallback}
124
+                    {...video}
125
+                  />
126
+                );
127
+              default:
128
+                return null;
129
+            }
130
+          })}
131
+        </div>
132
+      )}
133
+    </div>
134
+  );
135
+};
136
+
137
+const controls = [
138
+  "PlayPause",
139
+  "Seek",
140
+  "Time",
141
+  "Volume",
142
+  "Speed",
143
+  "Fullscreen",
144
+  "Captions"
145
+];
146
+
147
+DefaultPlayer.defaultProps = {
148
+  copy,
149
+  controls,
150
+  video: {}
151
+};
152
+
153
+DefaultPlayer.propTypes = {
154
+  copy: PropTypes.object.isRequired,
155
+  controls: PropTypes.arrayOf(PropTypes.oneOf(controls)),
156
+  video: PropTypes.object.isRequired
157
+};
158
+
159
+const connectedPlayer = videoConnect(
160
+  DefaultPlayer,
161
+  ({ networkState, readyState, error, ...restState }) => ({
162
+    video: {
163
+      readyState,
164
+      networkState,
165
+      error: error || (readyState > 0 && networkState === 3),
166
+      // TODO: This is not pretty. Doing device detection to remove
167
+      // spinner on iOS devices for a quick and dirty win. We should see if
168
+      // we can use the same readyState check safely across all browsers.
169
+      loading:
170
+        readyState < (/iPad|iPhone|iPod/.test(navigator.userAgent) ? 1 : 4),
171
+      percentagePlayed: getPercentagePlayed(restState),
172
+      percentageBuffered: getPercentageBuffered(restState),
173
+      ...restState
174
+    }
175
+  }),
176
+  (videoEl, state) => ({
177
+    onFullscreenClick: () => toggleFullscreen(videoEl.parentElement),
178
+    onVolumeClick: () => toggleMute(videoEl, state),
179
+    onCaptionsClick: () => toggleTracks(state),
180
+    onSpeedClick: () => toggleSpeeds(videoEl, state),
181
+    onPlayPauseClick: () => togglePause(videoEl, state),
182
+    onCaptionsItemClick: track => showTrack(state, track),
183
+    onSpeedsItemClick: speed => showSpeed(videoEl, state, speed),
184
+    onVolumeChange: e => setVolume(videoEl, state, e.target.value),
185
+    onSeekChange: e =>
186
+      setCurrentTime(videoEl, state, (e.target.value * state.duration) / 100)
187
+  })
188
+);
189
+
190
+export {
191
+  connectedPlayer as default,
192
+  DefaultPlayer,
193
+  Time,
194
+  Seek,
195
+  Volume,
196
+  Captions,
197
+  PlayPause,
198
+  Fullscreen,
199
+  Overlay
200
+};

+ 31
- 0
src/components/DefaultPlayer/DefaultPlayer.module.css View File

@@ -0,0 +1,31 @@
1
+.component {
2
+    position: relative;
3
+    font-family: Helvetica;
4
+    font-size: 11px;
5
+    background-color: #000;
6
+    width: 100%;
7
+}
8
+
9
+.video {
10
+    width: 100%;
11
+    height: 100%;
12
+}
13
+
14
+.controls {
15
+    position: absolute;
16
+    width: 100%;
17
+    bottom: 0;
18
+    height: 34px;
19
+    background-color: rgba(0, 0, 0, 0.7);
20
+    opacity: 0;
21
+    transition: opacity 0.2s;
22
+    display: flex;
23
+}
24
+
25
+.seek {
26
+    flex-grow: 1;
27
+}
28
+
29
+.component:hover .controls {
30
+    opacity: 1;
31
+}

+ 108
- 0
src/components/DefaultPlayer/DefaultPlayer.test.js View File

@@ -0,0 +1,108 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import { DefaultPlayer } from './DefaultPlayer';
4
+import styles from './DefaultPlayer.css';
5
+import Time from './Time/Time';
6
+import Seek from './Seek/Seek';
7
+import Volume from './Volume/Volume';
8
+import PlayPause from './PlayPause/PlayPause';
9
+import Fullscreen from './Fullscreen/Fullscreen';
10
+import Overlay from './Overlay/Overlay';
11
+
12
+describe('DefaultPlayer', () => {
13
+    let component;
14
+
15
+    beforeAll(() => {
16
+        component = shallow(<DefaultPlayer />);
17
+    });
18
+
19
+    it('should accept a className prop and append it to the components class', () => {
20
+        const newClassNameString = 'a new className';
21
+        expect(component.prop('className'))
22
+            .toContain(styles.component);
23
+        component.setProps({
24
+            className: newClassNameString
25
+        });
26
+        expect(component.prop('className'))
27
+            .toContain(styles.component);
28
+        expect(component.prop('className'))
29
+            .toContain(newClassNameString);
30
+    });
31
+
32
+    it('applies `style` prop if provided', () => {
33
+        component.setProps({
34
+            style: { color: 'red' }
35
+        });
36
+        expect(component.prop('style'))
37
+            .toEqual({ color: 'red' });
38
+    });
39
+
40
+    it('spreads all parent props on the video element', () => {
41
+        component.setProps({
42
+            autoPlay: true
43
+        });
44
+        expect(component.find('video').prop('autoPlay'))
45
+            .toEqual(true);
46
+    });
47
+
48
+    it('has an overlay component', () => {
49
+        expect(component.find(Overlay).exists())
50
+            .toBeTruthy();
51
+    });
52
+
53
+    it('renders some default controls in a default order', () => {
54
+        const controlsComponent = component.find(`.${styles.controls}`);
55
+        expect(controlsComponent.childAt(0).is(PlayPause))
56
+            .toBeTruthy();
57
+        expect(controlsComponent.childAt(1).is(Seek))
58
+            .toBeTruthy();
59
+        expect(controlsComponent.childAt(2).is(Time))
60
+            .toBeTruthy();
61
+        expect(controlsComponent.childAt(3).is(Volume))
62
+            .toBeTruthy();
63
+        expect(controlsComponent.childAt(4).is(Fullscreen))
64
+            .toBeTruthy();
65
+    });
66
+
67
+    it('renders controls in a given custom order', () => {
68
+        component.setProps({
69
+            controls: ['Fullscreen', 'Seek', 'Time']
70
+        });
71
+        const controlsComponent = component.find(`.${styles.controls}`);
72
+        expect(controlsComponent.childAt(0).is(Fullscreen))
73
+            .toBeTruthy();
74
+        expect(controlsComponent.childAt(1).is(Seek))
75
+            .toBeTruthy();
76
+        expect(controlsComponent.childAt(2).is(Time))
77
+            .toBeTruthy();
78
+    });
79
+
80
+    it('renders no controls when given an empty array', () => {
81
+        expect(component.find(`.${styles.controls}`).exists())
82
+            .toBeTruthy();
83
+        component.setProps({
84
+            controls: []
85
+        });
86
+        expect(component.find(`.${styles.controls}`).exists())
87
+            .toBeFalsy();
88
+    });
89
+
90
+    it('renders no controls when there is an error', () => {
91
+        component.setProps({
92
+            controls: ['PlayPause'],
93
+            video: {
94
+                error: false
95
+            }
96
+        });
97
+        expect(component.find(`.${styles.controls}`).exists())
98
+            .toBeTruthy();
99
+        component.setProps({
100
+            controls: ['PlayPause'],
101
+            video: {
102
+                error: true
103
+            }
104
+        });
105
+        expect(component.find(`.${styles.controls}`).exists())
106
+            .toBeFalsy();
107
+    });
108
+});

+ 18
- 0
src/components/DefaultPlayer/Fullscreen/Fullscreen.js View File

@@ -0,0 +1,18 @@
1
+import React from "react";
2
+import styles from "./Fullscreen.module.css";
3
+import { ReactComponent as FullscreenIcon } from "../Icon/fullscreen.svg";
4
+
5
+export default ({ onClick, className, ariaLabel, onScreenClickCallback }) => {
6
+  return (
7
+    <div className={[styles.component, className].join(" ")}>
8
+      <button
9
+        type="button"
10
+        onClick={onClick.bind(this, onScreenClickCallback)}
11
+        aria-label={ariaLabel}
12
+        className={styles.button}
13
+      >
14
+        <FullscreenIcon fill="#fff" className={styles.icon} />
15
+      </button>
16
+    </div>
17
+  );
18
+};

+ 25
- 0
src/components/DefaultPlayer/Fullscreen/Fullscreen.module.css View File

@@ -0,0 +1,25 @@
1
+.component:hover {
2
+    background-color: #000;
3
+}
4
+
5
+.button {
6
+    width: 34px;
7
+    height: 34px;
8
+    background: none;
9
+    border: 0;
10
+    color: inherit;
11
+    font: inherit;
12
+    cursor: pointer;
13
+    display: flex;
14
+    justify-content: center;
15
+    align-items: center;
16
+}
17
+
18
+.button:focus {
19
+    outline: 0;
20
+}
21
+
22
+.icon {
23
+    width: 24px;
24
+    height: 24px;
25
+}

+ 44
- 0
src/components/DefaultPlayer/Fullscreen/Fullscreen.test.js View File

@@ -0,0 +1,44 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Fullscreen from './Fullscreen';
4
+import styles from './Fullscreen.css';
5
+
6
+describe('Fullscreen', () => {
7
+    let component;
8
+
9
+    beforeAll(() => {
10
+        component = shallow(
11
+            <Fullscreen ariaLabel="Fullscreen" />
12
+        );
13
+    });
14
+
15
+    it('should accept a className prop and append it to the components class', () => {
16
+        const newClassNameString = 'a new className';
17
+        expect(component.prop('className'))
18
+            .toContain(styles.component);
19
+        component.setProps({
20
+            className: newClassNameString
21
+        });
22
+        expect(component.prop('className'))
23
+            .toContain(styles.component);
24
+        expect(component.prop('className'))
25
+            .toContain(newClassNameString);
26
+    });
27
+
28
+    it('triggers \'onClick\' prop when the button is clicked', () => {
29
+        const spy = jest.fn();
30
+        component.setProps({
31
+            onClick: spy
32
+        });
33
+        expect(spy)
34
+            .not.toHaveBeenCalled();
35
+        component.find('button').simulate('click');
36
+        expect(spy)
37
+            .toHaveBeenCalled();
38
+    });
39
+
40
+    it('has correct aria-label', () => {
41
+        expect(component.find('button').prop('aria-label'))
42
+            .toEqual('Fullscreen');
43
+    });
44
+});

+ 6
- 0
src/components/DefaultPlayer/Icon/caption_new.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="4 0 20 16">
5
+<path d="M0 0h26v22.677L21.544 17.8H0V0zm6.276 11.602a2.373 2.373 0 1 0 0-4.746 2.373 2.373 0 0 0 0 4.746zm6.592 0a2.373 2.373 0 1 0 0-4.746 2.373 2.373 0 0 0 0 4.746zm6.592 0a2.373 2.373 0 1 0 0-4.746 2.373 2.373 0 0 0 0 4.746z" fill="#FFF" fill-rule="evenodd"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/closed_caption.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
5
+<path d="M18 11.016v-1.031c0-0.563-0.422-0.984-0.984-0.984h-3c-0.563 0-1.031 0.422-1.031 0.984v4.031c0 0.563 0.469 0.984 1.031 0.984h3c0.563 0 0.984-0.422 0.984-0.984v-1.031h-1.5v0.516h-2.016v-3h2.016v0.516h1.5zM11.016 11.016v-1.031c0-0.563-0.469-0.984-1.031-0.984h-3c-0.563 0-0.984 0.422-0.984 0.984v4.031c0 0.563 0.422 0.984 0.984 0.984h3c0.563 0 1.031-0.422 1.031-0.984v-1.031h-1.5v0.516h-2.016v-3h2.016v0.516h1.5zM18.984 3.984c1.078 0 2.016 0.938 2.016 2.016v12c0 1.078-0.938 2.016-2.016 2.016h-13.969c-1.125 0-2.016-0.938-2.016-2.016v-12c0-1.078 0.891-2.016 2.016-2.016h13.969z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/fullscreen.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M14.016 5.016h4.969v4.969h-1.969v-3h-3v-1.969zM17.016 17.016v-3h1.969v4.969h-4.969v-1.969h3zM5.016 9.984v-4.969h4.969v1.969h-3v3h-1.969zM6.984 14.016v3h3v1.969h-4.969v-4.969h1.969z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/fullscreen_exit.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M15.984 8.016h3v1.969h-4.969v-4.969h1.969v3zM14.016 18.984v-4.969h4.969v1.969h-3v3h-1.969zM8.016 8.016v-3h1.969v4.969h-4.969v-1.969h3zM5.016 15.984v-1.969h4.969v4.969h-1.969v-3h-3z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/pause.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M14.016 5.016h3.984v13.969h-3.984v-13.969zM6 18.984v-13.969h3.984v13.969h-3.984z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/play_arrow.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M8.016 5.016l10.969 6.984-10.969 6.984v-13.969z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/report.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M12.984 12.984v-6h-1.969v6h1.969zM12 17.297c0.703 0 1.313-0.609 1.313-1.313s-0.609-1.266-1.313-1.266-1.313 0.563-1.313 1.266 0.609 1.313 1.313 1.313zM15.75 3l5.25 5.25v7.5l-5.25 5.25h-7.5l-5.25-5.25v-7.5l5.25-5.25h7.5z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/speed.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="14 8 12 24">
5
+<path d="M12.0791137,21.6949304 C12.406739,21.7334745 12.7729086,21.7527466 13.1583502,21.7720187 C13.4281593,21.7912908 13.6594243,21.9647395 13.7365126,22.2345486 C13.8714171,22.6585344 14.0255938,23.063248 14.2375867,23.4294176 C14.3724912,23.6606825 14.3339471,23.9497637 14.1604983,24.1617566 C13.9099613,24.4508378 13.6594243,24.739919 13.4667035,24.9904561 C13.2547106,25.2409931 13.2739827,25.6264347 13.5052476,25.8576997 L14.6423003,26.9947524 C14.8735653,27.2260173 15.2590069,27.2452894 15.5095439,27.0332965 C15.760081,26.8213037 16.0298901,26.5900387 16.3382434,26.3395017 C16.5502363,26.1660529 16.8393175,26.1275088 17.0705824,26.2624133 C17.456024,26.4551341 17.8607377,26.6285829 18.2654514,26.7634874 C18.5159884,26.8405757 18.7087092,27.0718407 18.7279813,27.3416498 C18.7472534,27.7270914 18.7857976,28.093261 18.8050696,28.4208863 C18.8436138,28.7485117 19.1134229,28.9990487 19.4410483,28.9990487 L21.059903,28.9990487 C21.3875284,28.9990487 21.6766096,28.7485117 21.6958816,28.4208863 C21.7344258,28.093261 21.7536979,27.7270914 21.77297,27.3416498 C21.792242,27.0718407 21.9656908,26.8405757 22.2354999,26.7634874 C22.6594857,26.6285829 23.0641993,26.4744062 23.4303689,26.2624133 C23.6616338,26.1275088 23.950715,26.1660529 24.1627079,26.3395017 C24.4517891,26.5900387 24.7408703,26.8405757 24.9914073,27.0332965 C25.2419444,27.2452894 25.627386,27.2260173 25.8586509,26.9947524 L26.9957037,25.8576997 C27.2269686,25.6264347 27.2462407,25.2409931 27.0342478,24.9904561 C26.822255,24.739919 26.59099,24.4701099 26.3404529,24.1617566 C26.1670042,23.9497637 26.1284601,23.6606825 26.2633646,23.4294176 C26.4560854,23.043976 26.6295341,22.6392623 26.7644387,22.2345486 C26.841527,21.9840116 27.072792,21.7912908 27.3426011,21.7720187 C27.7280427,21.7527466 28.0942122,21.7142024 28.4218376,21.6949304 C28.749463,21.6563862 29,21.3865771 29,21.0589517 L29,19.440097 C29,19.1124716 28.749463,18.8233904 28.4218376,18.8041184 C28.0942122,18.7655742 27.7280427,18.7463021 27.3426011,18.72703 C27.072792,18.707758 26.841527,18.5343092 26.7644387,18.2645001 C26.6295341,17.8405143 26.4753575,17.4358007 26.2633646,17.0696311 C26.1284601,16.8383662 26.1670042,16.549285 26.3404529,16.3372921 C26.59099,16.0482109 26.841527,15.7591297 27.0342478,15.5085927 C27.2462407,15.2580556 27.2269686,14.872614 26.9957037,14.6413491 L25.8586509,13.5042963 C25.627386,13.2730314 25.2419444,13.2537593 24.9914073,13.4657522 C24.7408703,13.677745 24.4710612,13.90901 24.1627079,14.1595471 C23.950715,14.3329958 23.6616338,14.3715399 23.4303689,14.2366354 C23.0449273,14.0439146 22.6402136,13.8704659 22.2354999,13.7355613 C21.9849628,13.658473 21.792242,13.427208 21.77297,13.1573989 C21.7536979,12.7719573 21.7151537,12.4057878 21.6958816,12.0781624 C21.6573375,11.750537 21.3875284,11.5 21.059903,11.5 L19.4410483,11.5 C19.1134229,11.5 18.8243417,11.750537 18.8050696,12.0781624 C18.7665255,12.4057878 18.7472534,12.7719573 18.7279813,13.1573989 C18.7087092,13.427208 18.5352605,13.658473 18.2654514,13.7355613 C17.8414656,13.8704659 17.436752,14.0246425 17.0705824,14.2366354 C16.8393175,14.3715399 16.5502363,14.3329958 16.3382434,14.1595471 C16.0491622,13.90901 15.760081,13.658473 15.5095439,13.4657522 C15.2590069,13.2537593 14.8735653,13.2730314 14.6423003,13.5042963 L13.5052476,14.6413491 C13.2739827,14.872614 13.2547106,15.2580556 13.4667035,15.5085927 C13.6786963,15.7591297 13.9099613,16.0289388 14.1604983,16.3372921 C14.3339471,16.549285 14.3724912,16.8383662 14.2375867,17.0696311 C14.0448659,17.4550727 13.8714171,17.8597864 13.7365126,18.2645001 C13.6594243,18.5150372 13.4281593,18.707758 13.1583502,18.72703 C12.7729086,18.7463021 12.406739,18.7848463 12.0791137,18.8041184 C11.7514883,18.8426625 11.5009513,19.1124716 11.5009513,19.440097 L11.5009513,21.0589517 C11.4816792,21.3865771 11.7322162,21.6756583 12.0791137,21.6949304 L12.0791137,21.6949304 Z M20.2504756,15.8747622 C22.6787577,15.8747622 24.6252378,17.8405143 24.6252378,20.2495244 C24.6252378,22.6778064 22.6594857,24.6242865 20.2504756,24.6242865 C17.8414656,24.6242865 15.8757135,22.6778064 15.8757135,20.2495244 C15.8757135,17.8212423 17.8221936,15.8747622 20.2504756,15.8747622 L20.2504756,15.8747622 Z"></path>
6
+</svg>

+ 1
- 0
src/components/DefaultPlayer/Icon/spin.svg View File

@@ -0,0 +1 @@
1
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><circle cx="50" cy="50" r="40" stroke="#757575" fill="none" stroke-width="10" stroke-linecap="round"></circle><circle cx="50" cy="50" r="40" stroke="#ffffff" fill="none" stroke-width="6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate><animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate></circle></svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/volume_down.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M5.016 9h3.984l5.016-5.016v16.031l-5.016-5.016h-3.984v-6zM18.516 12c0 1.781-1.031 3.281-2.531 4.031v-8.063c1.5 0.75 2.531 2.25 2.531 4.031z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/volume_mute.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M6.984 9h4.031l4.969-5.016v16.031l-4.969-5.016h-4.031v-6z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/volume_off.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M12 3.984v4.219l-2.109-2.109zM4.266 3l16.734 16.734-1.266 1.266-2.063-2.063c-1.078 0.844-2.297 1.5-3.656 1.828v-2.063c0.844-0.234 1.594-0.656 2.25-1.172l-4.266-4.266v6.75l-5.016-5.016h-3.984v-6h4.734l-4.734-4.734zM18.984 12c0-3.188-2.063-5.859-4.969-6.703v-2.063c4.031 0.891 6.984 4.5 6.984 8.766 0 1.5-0.375 2.953-1.031 4.172l-1.5-1.547c0.328-0.797 0.516-1.688 0.516-2.625zM16.5 12c0 0.234 0 0.422-0.047 0.609l-2.438-2.438v-2.203c1.5 0.75 2.484 2.25 2.484 4.031z"></path>
6
+</svg>

+ 6
- 0
src/components/DefaultPlayer/Icon/volume_up.svg View File

@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Generated by IcoMoon.io -->
3
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34" height="34" viewBox="0 0 24 24">
5
+<path d="M14.016 3.234c4.031 0.891 6.984 4.5 6.984 8.766s-2.953 7.875-6.984 8.766v-2.063c2.906-0.844 4.969-3.516 4.969-6.703s-2.063-5.859-4.969-6.703v-2.063zM16.5 12c0 1.781-0.984 3.281-2.484 4.031v-8.063c1.5 0.75 2.484 2.25 2.484 4.031zM3 9h3.984l5.016-5.016v16.031l-5.016-5.016h-3.984v-6z"></path>
6
+</svg>

+ 48
- 0
src/components/DefaultPlayer/Overlay/Overlay.js View File

@@ -0,0 +1,48 @@
1
+import React, { Component } from "react";
2
+import styles from "./Overlay.module.css";
3
+import { ReactComponent as PlayArrow } from "../Icon/play_arrow.svg";
4
+import { ReactComponent as Spin } from "../Icon/spin.svg";
5
+import { ReactComponent as Report } from "../Icon/report.svg";
6
+
7
+export default class Overlay extends Component {
8
+  renderContent() {
9
+    const { error, paused, loading } = this.props;
10
+    const iconProps = {
11
+      className: styles.icon,
12
+      height: 40,
13
+      width: 40,
14
+      fill: "#fff"
15
+    };
16
+    if (error) {
17
+      return (
18
+        <span className={styles.inner}>
19
+          <Report {...iconProps} />
20
+        </span>
21
+      );
22
+    } else if (loading) {
23
+      return (
24
+        <span className={styles.inner}>
25
+          <Spin {...iconProps} />
26
+        </span>
27
+      );
28
+    } else if (paused) {
29
+      return (
30
+        <span className={styles.inner}>
31
+          <PlayArrow {...iconProps} />
32
+        </span>
33
+      );
34
+    }
35
+  }
36
+
37
+  render() {
38
+    const { className, onClick } = this.props;
39
+    return (
40
+      <div
41
+        className={[styles.component, className].join(" ")}
42
+        onClick={onClick}
43
+      >
44
+        {this.renderContent()}
45
+      </div>
46
+    );
47
+  }
48
+}

+ 26
- 0
src/components/DefaultPlayer/Overlay/Overlay.module.css View File

@@ -0,0 +1,26 @@
1
+.component {
2
+    position: absolute;
3
+    top: 0;
4
+    right: 0;
5
+    bottom: 0;
6
+    left: 0;
7
+    height: 100%;
8
+    width: 100%;
9
+    color: #fff;
10
+    text-align: center;
11
+    cursor: pointer;
12
+    background-color: rgba(0, 0, 0, 0);
13
+    display: flex;
14
+    justify-content: center;
15
+    align-items: center;
16
+}
17
+
18
+.inner {
19
+    display: flex;
20
+    justify-content: center;
21
+    align-items: center;
22
+    width: 60px;
23
+    height: 60px;
24
+    background-color: rgba(0, 0, 0, 0.7);
25
+    border-radius: 10px;
26
+}

+ 56
- 0
src/components/DefaultPlayer/Overlay/Overlay.test.js View File

@@ -0,0 +1,56 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Overlay from './Overlay';
4
+import styles from './Overlay.css';
5
+import PlayArrow from './../Icon/play_arrow.svg';
6
+import Spin from './../Icon/spin.svg';
7
+import Report from './../Icon/report.svg';
8
+
9
+describe('Overlay', () => {
10
+    let component;
11
+
12
+    beforeAll(() => {
13
+        component = shallow(<Overlay />);
14
+    });
15
+
16
+    it('should accept a className prop and append it to the components class', () => {
17
+        const newClassNameString = 'a new className';
18
+        expect(component.prop('className'))
19
+            .toContain(styles.component);
20
+        component.setProps({
21
+            className: newClassNameString
22
+        });
23
+        expect(component.prop('className'))
24
+            .toContain(styles.component);
25
+        expect(component.prop('className'))
26
+            .toContain(newClassNameString);
27
+    });
28
+
29
+    it('shows a PlayArrow icon if paused', () => {
30
+        component.setProps({
31
+            paused: true
32
+        });
33
+        expect(component.html())
34
+            .toContain(PlayArrow);
35
+    });
36
+
37
+    it('shows Report icon if error regardless of loading or paused state', () => {
38
+        component.setProps({
39
+            error: true,
40
+            loading: true,
41
+            paused: true
42
+        });
43
+        expect(component.html())
44
+            .toContain(Report);
45
+    });
46
+
47
+    it('shows Spin icon if loading regardless of paused state', () => {
48
+        component.setProps({
49
+            loading: true,
50
+            paused: true,
51
+            error: false
52
+        });
53
+        expect(component.html())
54
+            .toContain(Spin);
55
+    });
56
+});

+ 29
- 0
src/components/DefaultPlayer/PlayPause/PlayPause.js View File

@@ -0,0 +1,29 @@
1
+import React from "react";
2
+import styles from "./PlayPause.module.css";
3
+import { ReactComponent as PlayArrow } from "../Icon/play_arrow.svg";
4
+import { ReactComponent as Pause } from "../Icon/pause.svg";
5
+
6
+export default ({
7
+  onClick,
8
+  paused,
9
+  className,
10
+  ariaLabelPlay,
11
+  ariaLabelPause
12
+}) => {
13
+  return (
14
+    <div className={[styles.component, className].join(" ")}>
15
+      <button
16
+        className={styles.button}
17
+        onClick={onClick}
18
+        aria-label={paused ? ariaLabelPlay : ariaLabelPause}
19
+        type="button"
20
+      >
21
+        {paused ? (
22
+          <PlayArrow className={styles.icon} fill="#fff" />
23
+        ) : (
24
+          <Pause className={styles.icon} fill="#fff" />
25
+        )}
26
+      </button>
27
+    </div>
28
+  );
29
+};

+ 25
- 0
src/components/DefaultPlayer/PlayPause/PlayPause.module.css View File

@@ -0,0 +1,25 @@
1
+.component:hover {
2
+    background-color: #000;
3
+}
4
+
5
+.button {
6
+    width: 34px;
7
+    height: 34px;
8
+    background: none;
9
+    border: 0;
10
+    color: inherit;
11
+    font: inherit;
12
+    line-height: normal;
13
+    overflow: visible;
14
+    padding: 0;
15
+    cursor: pointer;
16
+}
17
+
18
+.button:focus {
19
+    outline: 0;
20
+}
21
+
22
+.icon {
23
+    width: 24px;
24
+    height: 24px;
25
+}

+ 83
- 0
src/components/DefaultPlayer/PlayPause/PlayPause.test.js View File

@@ -0,0 +1,83 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import PlayPause from './PlayPause';
4
+import styles from './PlayPause.css';
5
+import PlayArrow from './../Icon/play_arrow.svg';
6
+import Pause from './../Icon/pause.svg';
7
+
8
+describe('PlayPause', () => {
9
+    let component;
10
+
11
+    beforeAll(() => {
12
+        component = shallow(
13
+            <PlayPause
14
+                ariaLabelPlay="Play"
15
+                ariaLabelPause="Pause" />
16
+        );
17
+    });
18
+
19
+    it('should accept a className prop and append it to the components class', () => {
20
+        const newClassNameString = 'a new className';
21
+        expect(component.prop('className'))
22
+            .toContain(styles.component);
23
+        component.setProps({
24
+            className: newClassNameString
25
+        });
26
+        expect(component.prop('className'))
27
+            .toContain(styles.component);
28
+        expect(component.prop('className'))
29
+            .toContain(newClassNameString);
30
+    });
31
+
32
+    it('triggers \'onClick\' prop when the button is clicked', () => {
33
+        const spy = jest.fn();
34
+        component.setProps({
35
+            onClick: spy
36
+        });
37
+        expect(spy)
38
+            .not.toHaveBeenCalled();
39
+        component.find('button').simulate('click');
40
+        expect(spy)
41
+            .toHaveBeenCalled();
42
+    });
43
+
44
+    describe('when paused', () => {
45
+        beforeAll(() => {
46
+            component.setProps({
47
+                paused: true
48
+            });
49
+        });
50
+
51
+        it('shows a play icon', () => {
52
+            expect(component.html())
53
+                .toContain(PlayArrow);
54
+            expect(component.html())
55
+                .not.toContain(Pause);
56
+        });
57
+
58
+        it('has correct aria-label', () => {
59
+            expect(component.find('button').prop('aria-label'))
60
+                .toEqual('Play');
61
+        });
62
+    });
63
+
64
+    describe('when playing', () => {
65
+        beforeAll(() => {
66
+            component.setProps({
67
+                paused: false
68
+            });
69
+        });
70
+
71
+        it('shows a pause icon', () => {
72
+            expect(component.html())
73
+                .toContain(Pause);
74
+            expect(component.html())
75
+                .not.toContain(PlayArrow);
76
+        });
77
+
78
+        it('has correct aria-label', () => {
79
+            expect(component.find('button').prop('aria-label'))
80
+                .toEqual('Pause');
81
+        });
82
+    });
83
+});

+ 40
- 0
src/components/DefaultPlayer/Seek/Seek.js View File

@@ -0,0 +1,40 @@
1
+import React from "react";
2
+import styles from "./Seek.module.css";
3
+
4
+export default ({
5
+  onChange,
6
+  percentagePlayed,
7
+  percentageBuffered,
8
+  className,
9
+  ariaLabel
10
+}) => {
11
+  return (
12
+    <div className={[styles.component, className].join(" ")}>
13
+      <div className={styles.track}>
14
+        <div
15
+          className={styles.buffer}
16
+          style={{
17
+            width: `${percentageBuffered || 0}%`
18
+          }}
19
+        />
20
+        <div
21
+          className={styles.fill}
22
+          style={{
23
+            width: `${percentagePlayed || 0}%`
24
+          }}
25
+        />
26
+        <input
27
+          min="0"
28
+          step={1}
29
+          max="100"
30
+          type="range"
31
+          orient="horizontal"
32
+          onChange={onChange}
33
+          aria-label={ariaLabel}
34
+          className={styles.input}
35
+          value={percentagePlayed || 0}
36
+        />
37
+      </div>
38
+    </div>
39
+  );
40
+};

+ 36
- 0
src/components/DefaultPlayer/Seek/Seek.module.css View File

@@ -0,0 +1,36 @@
1
+.component {
2
+    flex: 1;
3
+    display: flex;
4
+    justify-content: center;
5
+    align-items: center;
6
+    position: relative;
7
+}
8
+
9
+.track {
10
+    width: 100%;
11
+    height: 4px;
12
+    background-color: #3e3e3e;
13
+
14
+}
15
+
16
+.buffer,
17
+.fill,
18
+.input {
19
+    position: absolute;
20
+    width: 100%;
21
+    height: 4px;
22
+}
23
+
24
+.buffer {
25
+    background-color: #5a5a5a;
26
+}
27
+
28
+.fill {
29
+    background: #fff;
30
+}
31
+
32
+.input {
33
+    width: 100%;
34
+    opacity: 0;
35
+    cursor: pointer;
36
+}

+ 91
- 0
src/components/DefaultPlayer/Seek/Seek.test.js View File

@@ -0,0 +1,91 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Seek from './Seek';
4
+import styles from './Seek.css';
5
+
6
+describe('Seek', () => {
7
+    let component;
8
+
9
+    beforeAll(() => {
10
+        component = shallow(<Seek />);
11
+    });
12
+
13
+    it('should accept a className prop and append it to the components class', () => {
14
+        const newClassNameString = 'a new className';
15
+        expect(component.prop('className'))
16
+            .toContain(styles.component);
17
+        component.setProps({
18
+            className: newClassNameString
19
+        });
20
+        expect(component.prop('className'))
21
+            .toContain(styles.component);
22
+        expect(component.prop('className'))
23
+            .toContain(newClassNameString);
24
+    });
25
+
26
+    it('has a range input with correct ranges and percentagePlayed value', () => {
27
+        component.setProps({
28
+            percentagePlayed: 10
29
+        });
30
+        const rangeInput = component.find(`.${styles.input}`);
31
+        expect(rangeInput.prop('type'))
32
+            .toEqual('range');
33
+        expect(rangeInput.prop('orient'))
34
+            .toEqual('horizontal');
35
+        expect(rangeInput.prop('step'))
36
+            .toEqual(1);
37
+        expect(rangeInput.prop('min'))
38
+            .toEqual('0');
39
+        expect(rangeInput.prop('max'))
40
+            .toEqual('100');
41
+        expect(rangeInput.prop('value'))
42
+            .toEqual(10);
43
+    });
44
+
45
+    it('handles an undefined percentagePlayed value', () => {
46
+        component.setProps({
47
+            percentagePlayed: undefined
48
+        });
49
+        const rangeInput = component.find(`.${styles.input}`);
50
+        expect(rangeInput.prop('value'))
51
+            .toEqual(0);
52
+    });
53
+
54
+    it('triggers \'onChange\' prop when the input is changed', () => {
55
+        const spy = jest.fn();
56
+        component.setProps({
57
+            onChange: spy
58
+        });
59
+        expect(spy)
60
+            .not.toHaveBeenCalled();
61
+        component.find('input').simulate('change');
62
+        expect(spy)
63
+            .toHaveBeenCalled();
64
+    });
65
+
66
+    it('changes the played fill bar width', () => {
67
+        component.setProps({
68
+            percentagePlayed: 0
69
+        });
70
+        expect(component.find(`.${styles.fill}`).prop('style').width)
71
+            .toEqual('0%');
72
+        component.setProps({
73
+            percentagePlayed: 11
74
+        });
75
+        expect(component.find(`.${styles.fill}`).prop('style').width)
76
+            .toEqual('11%');
77
+    });
78
+
79
+    it('changes the buffer bar width', () => {
80
+        component.setProps({
81
+            percentageBuffered: 0
82
+        });
83
+        expect(component.find(`.${styles.buffer}`).prop('style').width)
84
+            .toEqual('0%');
85
+        component.setProps({
86
+            percentageBuffered: 11
87
+        });
88
+        expect(component.find(`.${styles.buffer}`).prop('style').width)
89
+            .toEqual('11%');
90
+    });
91
+});

+ 41
- 0
src/components/DefaultPlayer/Speed/Speed.js View File

@@ -0,0 +1,41 @@
1
+import React from "react";
2
+import styles from "./Speed.module.css";
3
+import { ReactComponent as SpeedIcon } from "../Icon/speed.svg";
4
+
5
+export default ({
6
+  playbackrates,
7
+  onClick,
8
+  onItemClick,
9
+  className,
10
+  ariaLabel
11
+}) => {
12
+  // console.log('speed')
13
+  return (
14
+    <div className={[styles.component, className].join(" ")}>
15
+      <button
16
+        type="button"
17
+        onClick={onClick}
18
+        aria-label={ariaLabel}
19
+        className={styles.button}
20
+      >
21
+        <SpeedIcon className={styles.icon} fill="#fff" />
22
+      </button>
23
+      <ul className={styles.speedList}>
24
+        {playbackrates &&
25
+          playbackrates.map(rate => (
26
+            <li
27
+              key={rate.id}
28
+              className={
29
+                rate.mode === "showing"
30
+                  ? styles.activeSpeedItem
31
+                  : styles.speedItem
32
+              }
33
+              onClick={onItemClick.bind(this, rate)}
34
+            >
35
+              {rate.name}
36
+            </li>
37
+          ))}
38
+      </ul>
39
+    </div>
40
+  );
41
+};

+ 58
- 0
src/components/DefaultPlayer/Speed/Speed.module.css View File

@@ -0,0 +1,58 @@
1
+.component:hover {
2
+    background-color: #000;
3
+}
4
+
5
+.button {
6
+    width: 34px;
7
+    height: 34px;
8
+    background: none;
9
+    border: 0;
10
+    color: inherit;
11
+    font: inherit;
12
+    line-height: normal;
13
+    overflow: visible;
14
+    padding: 0;
15
+    cursor: pointer;
16
+    display: flex;
17
+    justify-content: center;
18
+    align-items: center;
19
+}
20
+
21
+.button:focus {
22
+    outline: 0;
23
+}
24
+
25
+.icon {
26
+    width: 24px;
27
+    height: 24px;
28
+}
29
+
30
+.speedList {
31
+    position: absolute;
32
+    bottom: 100%;
33
+    display: none;
34
+    background-color: rgba(0, 0, 0, 0.7);
35
+    list-style: none;
36
+    padding: 0;
37
+    margin: 0;
38
+    color: #fff;
39
+}
40
+
41
+.component:hover .speedList {
42
+    display: block;
43
+}
44
+
45
+.speedItem {
46
+    padding: 7px;
47
+    cursor: pointer;
48
+}
49
+
50
+.activeSpeedItem,
51
+.speedItem:hover {
52
+    background: #000;
53
+}
54
+
55
+.activeSpeedItem {
56
+    composes: speedItem;
57
+    text-decoration: underline;
58
+}

+ 32
- 0
src/components/DefaultPlayer/Speed/Speed.test.js View File

@@ -0,0 +1,32 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Speed from './Speed';
4
+import styles from './Captions.css';
5
+
6
+describe('Speed', () => {
7
+    let component;
8
+
9
+    beforeAll(() => {
10
+        component = shallow(
11
+            <Speed ariaLabel="Speed" />
12
+        );
13
+    });
14
+
15
+    it('should accept a className prop and append it to the components class', () => {
16
+        const newClassNameString = 'a new className';
17
+        expect(component.prop('className'))
18
+            .toContain(styles.component);
19
+        component.setProps({
20
+            className: newClassNameString
21
+        });
22
+        expect(component.prop('className'))
23
+            .toContain(styles.component);
24
+        expect(component.prop('className'))
25
+            .toContain(newClassNameString);
26
+    });
27
+
28
+    it('has correct aria-label', () => {
29
+        expect(component.find('button').prop('aria-label'))
30
+            .toEqual('Speed');
31
+    });
32
+});

+ 22
- 0
src/components/DefaultPlayer/Time/Time.js View File

@@ -0,0 +1,22 @@
1
+import React from "react";
2
+import styles from "./Time.module.css";
3
+
4
+const formatTime = seconds => {
5
+  const date = new Date(Date.UTC(1970, 1, 1, 0, 0, 0, 0));
6
+  seconds = isNaN(seconds) || seconds > 86400 ? 0 : Math.floor(seconds);
7
+  date.setSeconds(seconds);
8
+  const duration = date
9
+    .toISOString()
10
+    .substr(11, 8)
11
+    .replace(/^0{1,2}:?0{0,1}/, "");
12
+  return duration;
13
+};
14
+
15
+export default ({ currentTime, duration, className }) => {
16
+  return (
17
+    <div className={[styles.component, className].join(" ")}>
18
+      <span className={styles.current}>{formatTime(currentTime)}</span>/
19
+      <span className={styles.duration}>{formatTime(duration)}</span>
20
+    </div>
21
+  );
22
+};

+ 14
- 0
src/components/DefaultPlayer/Time/Time.module.css View File

@@ -0,0 +1,14 @@
1
+.component {
2
+    padding: 0 10px 0 10px;
3
+    line-height: 35px;
4
+    color: #fff;
5
+}
6
+
7
+.current {
8
+    margin-right: 5px;
9
+}
10
+
11
+.duration {
12
+    margin-left: 5px;
13
+    color: #919191;
14
+}

+ 91
- 0
src/components/DefaultPlayer/Time/Time.test.js View File

@@ -0,0 +1,91 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Time from './Time';
4
+import styles from './Time.css';
5
+
6
+describe('Time', () => {
7
+    let component;
8
+
9
+    beforeAll(() => {
10
+        component = shallow(<Time />);
11
+    });
12
+
13
+    it('should accept a className prop and append it to the components class', () => {
14
+        const newClassNameString = 'a new className';
15
+        expect(component.prop('className'))
16
+            .toContain(styles.component);
17
+        component.setProps({
18
+            className: newClassNameString
19
+        });
20
+        expect(component.prop('className'))
21
+            .toContain(styles.component);
22
+        expect(component.prop('className'))
23
+            .toContain(newClassNameString);
24
+    });
25
+
26
+    it('shows video duration', () => {
27
+        component.setProps({
28
+            duration: 10
29
+        });
30
+        expect(component.find(`.${styles.duration}`).text())
31
+            .toEqual('0:10');
32
+    });
33
+
34
+    it('shows current video elapsed time', () => {
35
+        component.setProps({
36
+            currentTime: 10
37
+        });
38
+        expect(component.find(`.${styles.current}`).text())
39
+            .toEqual('0:10');
40
+    });
41
+
42
+    it('can handle minutes, hours and seconds', () => {
43
+        component.setProps({
44
+            currentTime: 60 * 2
45
+        });
46
+        expect(component.find(`.${styles.current}`).text())
47
+            .toEqual('2:00');
48
+
49
+        component.setProps({
50
+            currentTime: 60 * 60 * 3
51
+        });
52
+        expect(component.find(`.${styles.current}`).text())
53
+            .toEqual('3:00:00');
54
+
55
+        component.setProps({
56
+            currentTime: 60 * 60 * 3 + 72
57
+        });
58
+        expect(component.find(`.${styles.current}`).text())
59
+            .toEqual('3:01:12');
60
+    });
61
+
62
+    it('fails gracefully and shows 00:00:00 when video is greater than 24 hours', () => {
63
+        // Who has a video longer than 24 hours? If this ever occurs then we
64
+        // should consider adding it.
65
+        component.setProps({
66
+            currentTime: 86401
67
+        });
68
+        expect(component.find(`.${styles.current}`).text())
69
+            .toEqual('0:00');
70
+
71
+        component.setProps({
72
+            currentTime: 86400
73
+        });
74
+        expect(component.find(`.${styles.current}`).text())
75
+            .toEqual('0:00');
76
+    });
77
+
78
+    it('fails gracefully and shows 00:00 when not given a number', () => {
79
+        component.setProps({
80
+            currentTime: null
81
+        });
82
+        expect(component.find(`.${styles.current}`).text())
83
+            .toEqual('0:00');
84
+
85
+        component.setProps({
86
+            currentTime: undefined
87
+        });
88
+        expect(component.find(`.${styles.current}`).text())
89
+            .toEqual('0:00');
90
+    });
91
+});

+ 65
- 0
src/components/DefaultPlayer/Volume/Volume.js View File

@@ -0,0 +1,65 @@
1
+import React from "react";
2
+import styles from "./Volume.module.css";
3
+import { ReactComponent as VolumeOff } from "../Icon/volume_off.svg";
4
+import { ReactComponent as VolumeUp } from "../Icon/volume_up.svg";
5
+
6
+export default ({
7
+  onChange,
8
+  onClick,
9
+  volume,
10
+  muted,
11
+  className,
12
+  ariaLabelMute,
13
+  ariaLabelUnmute,
14
+  ariaLabelVolume
15
+}) => {
16
+  const volumeValue = muted || !volume ? 0 : +volume;
17
+  const isSilent = muted || volume <= 0;
18
+  return (
19
+    <div className={[styles.component, className].join(" ")}>
20
+      <button
21
+        aria-label={isSilent ? ariaLabelUnmute : ariaLabelMute}
22
+        className={styles.button}
23
+        onClick={onClick}
24
+        type="button"
25
+      >
26
+        {isSilent ? (
27
+          <VolumeOff
28
+            height={34}
29
+            width={34}
30
+            className={styles.icon}
31
+            fill="#fff"
32
+          />
33
+        ) : (
34
+          <VolumeUp
35
+            height={34}
36
+            width={34}
37
+            className={styles.icon}
38
+            fill="#fff"
39
+          />
40
+        )}
41
+      </button>
42
+      <div className={styles.slider}>
43
+        <div className={styles.track}>
44
+          <div
45
+            className={styles.fill}
46
+            style={{
47
+              height: `${volumeValue * 100}%`
48
+            }}
49
+          />
50
+          <input
51
+            min="0"
52
+            step={0.1}
53
+            max="1"
54
+            type="range"
55
+            orient="vertical"
56
+            onChange={onChange}
57
+            aria-label={ariaLabelVolume}
58
+            className={styles.input}
59
+            value={volumeValue}
60
+          />
61
+        </div>
62
+      </div>
63
+    </div>
64
+  );
65
+};

+ 77
- 0
src/components/DefaultPlayer/Volume/Volume.module.css View File

@@ -0,0 +1,77 @@
1
+.component {
2
+  position: relative;
3
+}
4
+.component:hover {
5
+  background-color: #000;
6
+}
7
+
8
+.button {
9
+  width: 34px;
10
+  height: 34px;
11
+  background: none;
12
+  border: 0;
13
+  color: inherit;
14
+  font: inherit;
15
+  line-height: normal;
16
+  overflow: visible;
17
+  padding: 0;
18
+  cursor: pointer;
19
+  display: flex;
20
+  justify-content: center;
21
+  align-items: center;
22
+}
23
+
24
+.button:focus {
25
+  outline: 0;
26
+}
27
+
28
+.icon {
29
+  width: 24px;
30
+  height: 24px;
31
+}
32
+
33
+.slider {
34
+  display: none;
35
+  position: absolute;
36
+  right: 5px;
37
+  bottom: 100%;
38
+  left: 5px;
39
+  height: 56px;
40
+  background-color: #000;
41
+}
42
+
43
+.component:hover .slider {
44
+  display: block;
45
+}
46
+
47
+.track {
48
+  position: absolute;
49
+  top: 8px;
50
+  bottom: 8px;
51
+  left: 50%;
52
+  width: 4px;
53
+  transform: translateX(-50%);
54
+  background-color: #3e3e3e;
55
+}
56
+
57
+.fill,
58
+.input {
59
+  position: absolute;
60
+  right: 0;
61
+  bottom: 0;
62
+  left: 0;
63
+  height: 100%;
64
+  width: 100%;
65
+}
66
+
67
+.fill {
68
+  background-color: #fff;
69
+}
70
+
71
+.input {
72
+  padding: 0;
73
+  margin: 0;
74
+  opacity: 0;
75
+  -webkit-appearance: slider-vertical;
76
+  cursor: pointer;
77
+}

+ 165
- 0
src/components/DefaultPlayer/Volume/Volume.test.js View File

@@ -0,0 +1,165 @@
1
+import React from 'react';
2
+import { shallow } from 'enzyme';
3
+import Volume from './Volume';
4
+import styles from './Volume.css';
5
+import VolumeOff from './../Icon/volume_off.svg';
6
+import VolumeUp from './../Icon/volume_up.svg';
7
+
8
+describe('Volume', () => {
9
+    let component;
10
+
11
+    beforeAll(() => {
12
+        component = shallow(
13
+            <Volume
14
+                ariaLabelMute="Mute"
15
+                ariaLabelUnmute="Unmute"
16
+                ariaLabelVolume="Change volume" />
17
+        );
18
+    });
19
+
20
+    it('should accept a className prop and append it to the components class', () => {
21
+        const newClassNameString = 'a new className';
22
+        expect(component.prop('className'))
23
+            .toContain(styles.component);
24
+        component.setProps({
25
+            className: newClassNameString
26
+        });
27
+        expect(component.prop('className'))
28
+            .toContain(styles.component);
29
+        expect(component.prop('className'))
30
+            .toContain(newClassNameString);
31
+    });
32
+
33
+    it('has a vertical range input with correct ranges and step', () => {
34
+        const rangeInput = component.find(`.${styles.input}`);
35
+        expect(rangeInput.prop('type'))
36
+            .toEqual('range');
37
+        expect(rangeInput.prop('orient'))
38
+            .toEqual('vertical');
39
+        expect(rangeInput.prop('step'))
40
+            .toEqual(0.1);
41
+        expect(rangeInput.prop('min'))
42
+            .toEqual('0');
43
+        expect(rangeInput.prop('max'))
44
+            .toEqual('1');
45
+        expect(rangeInput.prop('aria-label'))
46
+            .toEqual('Change volume');
47
+    });
48
+
49
+    it('handles an undefined volume value', () => {
50
+        component.setProps({
51
+            volume: undefined
52
+        });
53
+        const rangeInput = component.find(`.${styles.input}`);
54
+        expect(rangeInput.prop('value'))
55
+            .toEqual(0);
56
+    });
57
+
58
+    it('triggers \'onClick\' prop when the button is clicked', () => {
59
+        const spy = jest.fn();
60
+        component.setProps({
61
+            onClick: spy
62
+        });
63
+        expect(spy)
64
+            .not.toHaveBeenCalled();
65
+        component.find('button').simulate('click');
66
+        expect(spy)
67
+            .toHaveBeenCalled();
68
+    });
69
+
70
+    it('triggers \'onChange\' prop when the input is changed', () => {
71
+        const spy = jest.fn();
72
+        component.setProps({
73
+            onChange: spy
74
+        });
75
+        expect(spy)
76
+            .not.toHaveBeenCalled();
77
+        component.find('input').simulate('change');
78
+        expect(spy)
79
+            .toHaveBeenCalled();
80
+    });
81
+
82
+    describe('when muted', () => {
83
+        beforeAll(() => {
84
+            component.setProps({
85
+                muted: true,
86
+                volume: 0.5
87
+            });
88
+        });
89
+
90
+        it('shows a muted icon', () => {
91
+            expect(component.html())
92
+                .toContain(VolumeOff);
93
+            expect(component.html())
94
+                .not.toContain(VolumeUp);
95
+        });
96
+
97
+        it('has an empty track fill and range input', () => {
98
+            expect(component.find(`.${styles.fill}`).prop('style').height)
99
+                .toEqual('0%');
100
+            expect(component.find(`.${styles.input}`).prop('value'))
101
+                .toEqual(0);
102
+        });
103
+
104
+        it('has correct aria-label', () => {
105
+            expect(component.find('button').prop('aria-label'))
106
+                .toEqual('Unmute');
107
+        });
108
+    });
109
+
110
+    describe('when unmuted but has no volume', () => {
111
+        beforeAll(() => {
112
+            component.setProps({
113
+                muted: false,
114
+                volume: 0
115
+            });
116
+        });
117
+
118
+        it('shows a muted icon', () => {
119
+            expect(component.html())
120
+                .toContain(VolumeOff);
121
+            expect(component.html())
122
+                .not.toContain(VolumeUp);
123
+        });
124
+
125
+        it('has an empty track fill and range input', () => {
126
+            expect(component.find(`.${styles.fill}`).prop('style').height)
127
+                .toEqual('0%');
128
+            expect(component.find(`.${styles.input}`).prop('value'))
129
+                .toEqual(0);
130
+        });
131
+
132
+        it('has correct aria-label', () => {
133
+            expect(component.find('button').prop('aria-label'))
134
+                .toEqual('Unmute');
135
+        });
136
+    });
137
+
138
+    describe('when has volume and is not muted', () => {
139
+        beforeAll(() => {
140
+            component.setProps({
141
+                muted: false,
142
+                volume: 0.5
143
+            });
144
+        });
145
+
146
+        it('shows an unmute icon', () => {
147
+            expect(component.html())
148
+                .toContain(VolumeUp);
149
+            expect(component.html())
150
+                .not.toContain(VolumeOff);
151
+        });
152
+
153
+        it('has some track filled and a range input value', () => {
154
+            expect(component.find(`.${styles.fill}`).prop('style').height)
155
+                .toEqual('50%');
156
+            expect(component.find(`.${styles.input}`).prop('value'))
157
+                .toEqual(0.5);
158
+        });
159
+
160
+        it('has correct aria-label', () => {
161
+            expect(component.find('button').prop('aria-label'))
162
+                .toEqual('Mute');
163
+        });
164
+    });
165
+});

+ 12
- 0
src/components/DefaultPlayer/copy.js View File

@@ -0,0 +1,12 @@
1
+const copy = {
2
+    play: 'Play video',
3
+    pause: 'Pause video',
4
+    mute: 'Mute video',
5
+    unmute: 'Unmute video',
6
+    volume: 'Change volume',
7
+    fullscreen: 'View video fullscreen',
8
+    seek: 'Seek video',
9
+    captions: 'Toggle captions'
10
+};
11
+
12
+export default copy;

+ 24
- 0
src/components/entry.js View File

@@ -0,0 +1,24 @@
1
+import videoConnect from './video/video';
2
+import * as apiHelpers from './video/api';
3
+import DefaultPlayer, {
4
+    Time,
5
+    Seek,
6
+    Volume,
7
+    Captions,
8
+    PlayPause,
9
+    Fullscreen,
10
+    Overlay
11
+} from './DefaultPlayer/DefaultPlayer';
12
+
13
+export {
14
+    videoConnect as default,
15
+    apiHelpers,
16
+    DefaultPlayer,
17
+    Time,
18
+    Seek,
19
+    Volume,
20
+    Captions,
21
+    PlayPause,
22
+    Fullscreen,
23
+    Overlay
24
+};

+ 145
- 0
src/components/video/api.js View File

@@ -0,0 +1,145 @@
1
+/**
2
+ * These are custom helper methods that are not native
3
+ * to the HTMLMediaElement API. Pass in the native
4
+ * Video element, state and optional desired value to
5
+ * set. To be primarily used in `mapVideoElToProps`.
6
+ */
7
+export const togglePause = (videoEl, { paused }) => {
8
+  const videoTitleEl = document.getElementById("videoTitle");
9
+
10
+  paused ? videoEl.play() : videoEl.pause();
11
+
12
+  if (videoTitleEl) {
13
+    videoTitleEl.style.display = paused ? "none" : "";
14
+  }
15
+};
16
+
17
+export const setCurrentTime = (videoEl, state, value) => {
18
+  videoEl.currentTime = value;
19
+};
20
+
21
+export const setVolume = (videoEl, state, value) => {
22
+  videoEl.muted = false;
23
+  videoEl.volume = value;
24
+};
25
+
26
+export const mute = videoEl => {
27
+  videoEl.muted = true;
28
+};
29
+
30
+export const unmute = videoEl => {
31
+  videoEl.muted = false;
32
+};
33
+
34
+export const toggleMute = (videoEl, { volume, muted }) => {
35
+  if (muted || volume <= 0) {
36
+    if (volume <= 0) {
37
+      videoEl.volume = 1;
38
+    }
39
+    videoEl.muted = false;
40
+  } else {
41
+    videoEl.muted = true;
42
+  }
43
+};
44
+
45
+export const toggleFullscreen = (videoEl, callback) => {
46
+  videoEl.requestFullScreen =
47
+    videoEl.requestFullscreen ||
48
+    videoEl.msRequestFullscreen ||
49
+    videoEl.mozRequestFullScreen ||
50
+    videoEl.webkitRequestFullscreen;
51
+  document.exitFullscreen =
52
+    document.exitFullscreen ||
53
+    document.msExitFullscreen ||
54
+    document.mozCancelFullScreen ||
55
+    document.webkitExitFullscreen;
56
+  const fullscreenElement =
57
+    document.fullscreenElement ||
58
+    document.msFullscreenElement ||
59
+    document.mozFullScreenElement ||
60
+    document.webkitFullscreenElement;
61
+  if (fullscreenElement === videoEl) {
62
+    document.querySelector("video").style.maxHeight = "100%";
63
+    window.fullscreen = false;
64
+    document.exitFullscreen();
65
+  } else {
66
+    document.querySelector("video").style.maxHeight = "100%";
67
+    window.fullscreen = true;
68
+    videoEl.requestFullScreen();
69
+  }
70
+};
71
+
72
+export const showTrack = ({ textTracks }, track) => {
73
+  hideTracks({ textTracks });
74
+  track.mode = track.SHOWING || "showing";
75
+};
76
+
77
+export const hideTracks = ({ textTracks }) => {
78
+  for (var i = 0; i < textTracks.length; i++) {
79
+    textTracks[i].mode = textTracks[i].DISABLED || "disabled";
80
+  }
81
+};
82
+
83
+export const toggleTracks = (() => {
84
+  let previousTrack;
85
+  return ({ textTracks }) => {
86
+    let currentTrack = [...textTracks].filter(
87
+      track => track.mode === track.SHOWING || track.mode === "showing"
88
+    )[0];
89
+    if (currentTrack) {
90
+      hideTracks({ textTracks });
91
+      previousTrack = currentTrack;
92
+    } else {
93
+      showTrack({ textTracks }, previousTrack || textTracks[0]);
94
+    }
95
+  };
96
+})();
97
+
98
+export const showSpeed = (videoEl, state, speed) => {
99
+  let playbackrates = state.playbackrates;
100
+  hideSpeeds(videoEl, { playbackrates });
101
+  speed.mode = speed.SHOWING || "showing";
102
+  if (videoEl.dataset) {
103
+    videoEl.dataset["playbackrates"] = JSON.stringify(playbackrates);
104
+  } else {
105
+    videoEl.setAttribute("data-playbackrates", JSON.stringify(playbackrates));
106
+  }
107
+  videoEl.playbackRate = speed.id;
108
+};
109
+
110
+export const hideSpeeds = (videoEl, state) => {
111
+  let playbackrates = state.playbackrates;
112
+  for (var i = 0; i < playbackrates.length; i++) {
113
+    playbackrates[i].mode = playbackrates[i].DISABLED || "disabled";
114
+  }
115
+};
116
+
117
+export const toggleSpeeds = (() => {
118
+  let previousSpeed;
119
+  return (videoEl, state) => {
120
+    let playbackrates = state.playbackrates;
121
+
122
+    let currentSpeed = playbackrates.filter(item => item.mode === "showing")[0];
123
+
124
+    if (currentSpeed) {
125
+      hideSpeeds(videoEl, { playbackrates });
126
+      previousSpeed = currentSpeed;
127
+    } else {
128
+      showSpeed(videoEl, { playbackrates }, previousSpeed || playbackrates[0]);
129
+    }
130
+  };
131
+})();
132
+
133
+/**
134
+ * Custom getter methods that are commonly used
135
+ * across video layouts. To be primarily used in
136
+ * `mapStateToProps`
137
+ */
138
+export const getPercentageBuffered = ({ buffered, duration }) =>
139
+  (buffered &&
140
+    buffered.length &&
141
+    (buffered.end(buffered.length - 1) / duration) * 100) ||
142
+  0;
143
+
144
+export const getPercentagePlayed = ({ currentTime, duration }) =>
145
+  (currentTime / duration) * 100;

+ 276
- 0
src/components/video/api.test.js View File

@@ -0,0 +1,276 @@
1
+import {
2
+    mute,
3
+    unmute,
4
+    setVolume,
5
+    showTrack,
6
+    toggleMute,
7
+    hideTracks,
8
+    togglePause,
9
+    toggleTracks,
10
+    setCurrentTime,
11
+    toggleFullscreen,
12
+    getPercentagePlayed
13
+} from './api';
14
+
15
+describe('api', () => {
16
+    let videoElMock;
17
+    let textTracksMock;
18
+
19
+    beforeEach(() => {
20
+        videoElMock = {
21
+            play: jest.fn(),
22
+            pause: jest.fn()
23
+        };
24
+        textTracksMock = [{
25
+            id: 1,
26
+            mode: 'showing'
27
+        }, {
28
+            id: 2,
29
+            mode: 'disabled'
30
+        }, {
31
+            id: 3,
32
+            mode: 'disabled'
33
+        }];
34
+    });
35
+
36
+    describe('togglePause', () => {
37
+        it('plays if the video is paused', () => {
38
+            expect(videoElMock.play).not.toHaveBeenCalled();
39
+            togglePause(videoElMock, { paused: true });
40
+            expect(videoElMock.play).toHaveBeenCalled();
41
+            expect(videoElMock.pause).not.toHaveBeenCalled();
42
+        });
43
+
44
+        it('pauses if the video is playing', () => {
45
+            expect(videoElMock.pause).not.toHaveBeenCalled();
46
+            togglePause(videoElMock, { paused: false });
47
+            expect(videoElMock.pause).toHaveBeenCalled();
48
+            expect(videoElMock.play).not.toHaveBeenCalled();
49
+        });
50
+    });
51
+
52
+    describe('setCurrentTime', () => {
53
+        it('sets the current time', () => {
54
+            expect(videoElMock.currentTime).toBe(undefined);
55
+            setCurrentTime(videoElMock, undefined, 10);
56
+            expect(videoElMock.currentTime).toBe(10);
57
+        });
58
+    });
59
+
60
+    describe('setVolume', () => {
61
+        it('unmutes', () => {
62
+            expect(videoElMock.muted).toBe(undefined);
63
+            setVolume(videoElMock, undefined, 10);
64
+            expect(videoElMock.muted).toBe(false);
65
+        });
66
+
67
+        it('sets the volume', () => {
68
+            expect(videoElMock.volume).toBe(undefined);
69
+            setVolume(videoElMock, undefined, 10);
70
+            expect(videoElMock.volume).toBe(10);
71
+        });
72
+    });
73
+
74
+    describe('mute', () => {
75
+        it('mutes', () => {
76
+            expect(videoElMock.muted).toBe(undefined);
77
+            mute(videoElMock);
78
+            expect(videoElMock.muted).toBe(true);
79
+        });
80
+    });
81
+
82
+    describe('unmute', () => {
83
+        it('unmutes', () => {
84
+            expect(videoElMock.muted).toBe(undefined);
85
+            unmute(videoElMock);
86
+            expect(videoElMock.muted).toBe(false);
87
+        });
88
+    });
89
+
90
+    describe('toggleMute', () => {
91
+        it('unmutes if muted and does not change volume', () => {
92
+            expect(videoElMock.muted).toBe(undefined);
93
+            toggleMute(videoElMock, { volume: 0.5, muted: true });
94
+            expect(videoElMock.volume).toBe(undefined);
95
+            expect(videoElMock.muted).toBe(false);
96
+        });
97
+
98
+        it('mutes if unmuted and does not change volume', () => {
99
+            expect(videoElMock.muted).toBe(undefined);
100
+            toggleMute(videoElMock, { volume: 0.5, muted: false });
101
+            expect(videoElMock.volume).toBe(undefined);
102
+            expect(videoElMock.muted).toBe(true);
103
+        });
104
+
105
+        it('sets volume to max if volume is 0', () => {
106
+            expect(videoElMock.muted).toBe(undefined);
107
+            expect(videoElMock.volume).toBe(undefined);
108
+            toggleMute(videoElMock, { volume: 0, muted: false });
109
+            expect(videoElMock.volume).toBe(1);
110
+            expect(videoElMock.muted).toBe(false);
111
+        });
112
+
113
+        it('unmutes and sets volume to max if volume is 0', () => {
114
+            expect(videoElMock.muted).toBe(undefined);
115
+            expect(videoElMock.volume).toBe(undefined);
116
+            toggleMute(videoElMock, { volume: 0, muted: true });
117
+            expect(videoElMock.volume).toBe(1);
118
+            expect(videoElMock.muted).toBe(false);
119
+        });
120
+    });
121
+
122
+    describe('toggleFullscreen', () => {
123
+        describe('going fullscreen', () => {
124
+            it('requestsFullscreen', () => {
125
+                videoElMock.requestFullscreen = jest.fn();
126
+                toggleFullscreen(videoElMock);
127
+                expect(videoElMock.requestFullscreen).toHaveBeenCalled();
128
+            });
129
+
130
+            it('requestsFullscreen for ms', () => {
131
+                videoElMock.msRequestFullscreen = jest.fn();
132
+                toggleFullscreen(videoElMock);
133
+                expect(videoElMock.msRequestFullscreen).toHaveBeenCalled();
134
+            });
135
+
136
+            it('requestsFullscreen for moz', () => {
137
+                videoElMock.mozRequestFullScreen = jest.fn();
138
+                toggleFullscreen(videoElMock);
139
+                expect(videoElMock.mozRequestFullScreen).toHaveBeenCalled();
140
+            });
141
+
142
+            it('requestsFullscreen for webkit', () => {
143
+                videoElMock.webkitRequestFullscreen = jest.fn();
144
+                toggleFullscreen(videoElMock);
145
+                expect(videoElMock.webkitRequestFullscreen).toHaveBeenCalled();
146
+            });
147
+        });
148
+
149
+        describe('exiting fullscreen', () => {
150
+            beforeEach(() => {
151
+                document.exitFullscreen = undefined;
152
+                document.msExitFullscreen = undefined;
153
+                document.mozCancelFullScreen = undefined;
154
+                document.webkitExitFullscreen = undefined;
155
+                document.fullscreenElement = undefined;
156
+                document.msFullscreenElement = undefined;
157
+                document.mozFullScreenElement = undefined;
158
+                document.webkitFullscreenElement = undefined;
159
+            });
160
+
161
+            it('exitFullscreen', () => {
162
+                document.fullscreenElement = videoElMock;
163
+                document.exitFullscreen = jest.fn();
164
+                toggleFullscreen(videoElMock);
165
+                expect(document.exitFullscreen).toHaveBeenCalled();
166
+            });
167
+
168
+            it('exitFullscreen for ms', () => {
169
+                document.msFullscreenElement = videoElMock;
170
+                document.msExitFullscreen = jest.fn();
171
+                toggleFullscreen(videoElMock);
172
+                expect(document.msExitFullscreen).toHaveBeenCalled();
173
+            });
174
+
175
+            it('exitFullscreen for moz', () => {
176
+                document.mozFullScreenElement = videoElMock;
177
+                document.mozCancelFullScreen = jest.fn();
178
+                toggleFullscreen(videoElMock);
179
+                expect(document.mozCancelFullScreen).toHaveBeenCalled();
180
+            });
181
+
182
+            it('exitFullscreen for webkit', () => {
183
+                document.webkitFullscreenElement = videoElMock;
184
+                document.webkitExitFullscreen = jest.fn();
185
+                toggleFullscreen(videoElMock);
186
+                expect(document.webkitExitFullscreen).toHaveBeenCalled();
187
+            });
188
+        });
189
+    });
190
+
191
+    describe('hideTracks', () => {
192
+        it('hides all of the tracks', () => {
193
+            expect(textTracksMock[0].mode).toBe('showing');
194
+            hideTracks({ textTracks: textTracksMock });
195
+            expect(textTracksMock[0].mode).toBe('disabled');
196
+        });
197
+
198
+        it('uses constants on text tracks if they exist for IE', () => {
199
+            textTracksMock[0].DISABLED = 3;
200
+            expect(textTracksMock[0].mode).toBe('showing');
201
+            hideTracks({ textTracks: textTracksMock });
202
+            expect(textTracksMock[0].mode).toBe(3);
203
+        });
204
+    });
205
+
206
+    describe('showTrack', () => {
207
+        it('hides all of the tracks', () => {
208
+            expect(textTracksMock[0].mode).toBe('showing');
209
+            showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
210
+            expect(textTracksMock[0].mode).toBe('disabled');
211
+        });
212
+
213
+        it('sets the given track to show', () => {
214
+            expect(textTracksMock[2].mode).toBe('disabled');
215
+            showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
216
+            expect(textTracksMock[2].mode).toBe('showing');
217
+        });
218
+
219
+        it('uses constants on text tracks if they exist for IE', () => {
220
+            textTracksMock[2].SHOWING = 2;
221
+            expect(textTracksMock[2].mode).toBe('disabled');
222
+            showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
223
+            expect(textTracksMock[2].mode).toBe(2);
224
+        });
225
+    });
226
+
227
+    describe('toggleTracks', () => {
228
+        it('shows the first track if no tracks are showing and there is no previously active track', () => {
229
+            textTracksMock[0].mode = 'disabled';
230
+            expect(textTracksMock[0].mode).toBe('disabled');
231
+            toggleTracks({ textTracks: textTracksMock });
232
+            expect(textTracksMock[0].mode).toBe('showing');
233
+        });
234
+
235
+        it('hides all tracks if a current track is showing', () => {
236
+            expect(textTracksMock[0].mode).toBe('showing');
237
+            toggleTracks({ textTracks: textTracksMock });
238
+            expect(textTracksMock[0].mode).toBe('disabled');
239
+            expect(textTracksMock[1].mode).toBe('disabled');
240
+            expect(textTracksMock[2].mode).toBe('disabled');
241
+        });
242
+
243
+        it('shows the previously active track if no tracks are showing', () => {
244
+            expect(textTracksMock[0].mode).toBe('showing');
245
+            toggleTracks({ textTracks: textTracksMock });
246
+            expect(textTracksMock[0].mode).toBe('disabled');
247
+            toggleTracks({ textTracks: textTracksMock });
248
+            expect(textTracksMock[0].mode).toBe('showing');
249
+            showTrack({ textTracks: textTracksMock }, textTracksMock[2]);
250
+            expect(textTracksMock[2].mode).toBe('showing');
251
+            toggleTracks({ textTracks: textTracksMock });
252
+            expect(textTracksMock[2].mode).toBe('disabled');
253
+            toggleTracks({ textTracks: textTracksMock });
254
+            expect(textTracksMock[2].mode).toBe('showing');
255
+        });
256
+    });
257
+
258
+    describe('getPercentagePlayed', () => {
259
+        it('returns correct percentage played', () => {
260
+            expect(getPercentagePlayed({
261
+                currentTime: 10,
262
+                duration: 100
263
+            })).toBe(10);
264
+
265
+            expect(getPercentagePlayed({
266
+                currentTime: 1,
267
+                duration: 10
268
+            })).toBe(10);
269
+
270
+            expect(getPercentagePlayed({
271
+                currentTime: 5,
272
+                duration: 20
273
+            })).toBe(25);
274
+        });
275
+    });
276
+});

+ 71
- 0
src/components/video/constants.js View File

@@ -0,0 +1,71 @@
1
+export const EVENTS = [
2
+    'abort',
3
+    'canPlay',
4
+    'canPlayThrough',
5
+    'durationChange',
6
+    'emptied',
7
+    'encrypted',
8
+    'ended',
9
+    'error',
10
+    'loadedData',
11
+    'loadedMetadata',
12
+    'loadStart',
13
+    'pause',
14
+    'play',
15
+    'playing',
16
+    'progress',
17
+    'rateChange',
18
+    'seeked',
19
+    'seeking',
20
+    'stalled',
21
+    'suspend',
22
+    'timeUpdate',
23
+    'volumeChange',
24
+    'waiting'
25
+];
26
+
27
+export const TRACKEVENTS = [
28
+    'change',
29
+    'addTrack',
30
+    'removeTrack'
31
+];
32
+
33
+export const METHODS = [
34
+    'addTextTrack',
35
+    'canPlayType',
36
+    'load',
37
+    'play',
38
+    'pause'
39
+];
40
+
41
+export const PROPERTIES = [
42
+    'audioTracks',
43
+    'autoPlay',
44
+    'buffered',
45
+    'controller',
46
+    'controls',
47
+    'currentSrc',
48
+    'currentTime',
49
+    'defaultMuted',
50
+    'defaultPlaybackRate',
51
+    'playbackrates',
52
+    'duration',
53
+    'ended',
54
+    'error',
55
+    'loop',
56
+    'mediaGroup',
57
+    'muted',
58
+    'networkState',
59
+    'paused',
60
+    'playbackRate',
61
+    'played',
62
+    'preload',
63
+    'readyState',
64
+    'seekable',
65
+    'seeking',
66
+    'src',
67
+    'startDate',
68
+    'textTracks',
69
+    'videoTracks',
70
+    'volume'
71
+];

+ 153
- 0
src/components/video/video.js View File

@@ -0,0 +1,153 @@
1
+/**
2
+ * This is a HoC that finds a single
3
+ * <video> in a component and makes
4
+ * all its PROPERTIES available as props.
5
+ */
6
+import React, { Component } from "react";
7
+import { findDOMNode } from "react-dom";
8
+import { EVENTS, PROPERTIES, TRACKEVENTS } from "./constants";
9
+
10
+const defaultMapStateToProps = (state = {}) =>
11
+  Object.assign({
12
+    video: {
13
+      ...state
14
+    }
15
+  });
16
+
17
+const defaultMapVideoElToProps = videoEl => ({
18
+  videoEl
19
+});
20
+
21
+const defaultMergeProps = (stateProps = {}, videoElProps = {}, ownProps = {}) =>
22
+  Object.assign({}, stateProps, videoElProps, ownProps);
23
+
24
+export default (
25
+  BaseComponent,
26
+  mapStateToProps = defaultMapStateToProps,
27
+  mapVideoElToProps = defaultMapVideoElToProps,
28
+  mergeProps = defaultMergeProps
29
+) =>
30
+  class Video extends Component {
31
+    constructor(props) {
32
+      super(props);
33
+      this.updateState = this.updateState.bind(this);
34
+      this.state = {};
35
+    }
36
+
37
+    updateState() {
38
+      this.setState(
39
+        PROPERTIES.reduce((p, c) => {
40
+          p[c] = this.videoEl && this.videoEl[c];
41
+          if (c === "playbackrates" && this.videoEl) {
42
+            if (this.videoEl.dataset && this.videoEl.dataset[c]) {
43
+              p[c] = JSON.parse(this.videoEl.dataset[c]);
44
+            } else {
45
+              p[c] = JSON.parse(this.videoEl.getAttribute("data-" + c));
46
+            }
47
+          }
48
+          return p;
49
+        }, {})
50
+      );
51
+    }
52
+
53
+    bindEventsToUpdateState() {
54
+      EVENTS.forEach(event => {
55
+        if (this.videoEl.addEventListener) {
56
+          this.videoEl.addEventListener(event.toLowerCase(), this.updateState);
57
+        } else {
58
+          this.videoEl.attachEvent(
59
+            "on" + event.toLowerCase(),
60
+            this.updateState
61
+          );
62
+        }
63
+      });
64
+
65
+      TRACKEVENTS.forEach(event => {
66
+        // TODO: JSDom does not have this method on
67
+        // `textTracks`. Investigate so we can test this without this check.
68
+        this.videoEl.textTracks &&
69
+          this.videoEl.textTracks.addEventListener &&
70
+          this.videoEl.textTracks.addEventListener(
71
+            event.toLowerCase(),
72
+            this.updateState
73
+          );
74
+      });
75
+
76
+      // If <source> elements are used instead of a src attribute then
77
+      // errors for unsupported format do not bubble up to the <video>.
78
+      // Do this manually by listening to the last <source> error event
79
+      // to force an update.
80
+      // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video
81
+      const sources = this.videoEl.getElementsByTagName("source");
82
+      if (sources.length) {
83
+        const lastSource = sources[sources.length - 1];
84
+        lastSource.addEventListener
85
+          ? lastSource.addEventListener("error", this.updateState)
86
+          : lastSource.attachEvent("error", this.updateState);
87
+      }
88
+    }
89
+
90
+    unbindEvents() {
91
+      EVENTS.forEach(event => {
92
+        this.videoEl.removeEventListener
93
+          ? this.videoEl.removeEventListener(
94
+              event.toLowerCase(),
95
+              this.updateState
96
+            )
97
+          : this.videoEl.detachEvent(
98
+              "on" + event.toLowerCase(),
99
+              this.updateState
100
+            );
101
+      });
102
+
103
+      TRACKEVENTS.forEach(event => {
104
+        // TODO: JSDom does not have this method on
105
+        // `textTracks`. Investigate so we can test this without this check.
106
+        this.videoEl.textTracks &&
107
+          this.videoEl.textTracks.removeEventListener &&
108
+          this.videoEl.textTracks.removeEventListener(
109
+            event.toLowerCase(),
110
+            this.updateState
111
+          );
112
+      });
113
+
114
+      const sources = this.videoEl.getElementsByTagName("source");
115
+      if (sources.length) {
116
+        const lastSource = sources[sources.length - 1];
117
+        lastSource.removeEventListener
118
+          ? lastSource.removeEventListener("error", this.updateState)
119
+          : lastSource.detachEvent("onerror", this.updateState);
120
+      }
121
+    }
122
+
123
+    componentWillUnmount() {
124
+      this.unbindEvents();
125
+    }
126
+
127
+    // Stop `this.el` from being null briefly on every render,
128
+    // see: https://github.com/mderrick/react-html5video/pull/65
129
+    setRef(el) {
130
+      this.el = findDOMNode(el);
131
+    }
132
+
133
+    componentDidMount() {
134
+      this.videoEl = this.el.getElementsByTagName("video")[0];
135
+      this.bindEventsToUpdateState();
136
+    }
137
+
138
+    render() {
139
+      const stateProps = mapStateToProps(this.state, this.props);
140
+      const videoElProps = mapVideoElToProps(
141
+        this.videoEl,
142
+        this.state,
143
+        this.props
144
+      );
145
+      return (
146
+        <div ref={this.setRef.bind(this)} style={{ height: "100%" }}>
147
+          <BaseComponent
148
+            {...mergeProps(stateProps, videoElProps, this.props)}
149
+          />
150
+        </div>
151
+      );
152
+    }
153
+  };

+ 234
- 0
src/components/video/video.test.js View File

@@ -0,0 +1,234 @@
1
+import React from 'react';
2
+import { mount, shallow } from 'enzyme';
3
+import video from './video';
4
+import { EVENTS } from './constants';
5
+
6
+const TestControl = ({ duration }) => {
7
+    return (
8
+        <div>
9
+            { duration }
10
+        </div>
11
+    );
12
+};
13
+
14
+const TestVideo = ({ video, ...restProps }) => {
15
+    // Remove `videoEl` so we do not spread an unsupported
16
+    // prop onto a DOM element.
17
+    delete restProps.videoEl;
18
+    return (
19
+        <div>
20
+            <video {...restProps}>
21
+                <source src="1" />
22
+            </video>
23
+            <TestControl {...video} />
24
+        </div>
25
+    );
26
+};
27
+
28
+describe('video', () => {
29
+    let Component;
30
+    let component;
31
+
32
+    beforeAll(() => {
33
+        Component = video(TestVideo);
34
+    });
35
+
36
+    describe('the wrapped component', () => {
37
+        beforeEach(() => {
38
+            component = mount(
39
+                <Component autoPlay />
40
+            );
41
+        });
42
+
43
+        describe('HTMLMediaElement API as props', () => {
44
+            let testControl;
45
+            beforeEach(() => {
46
+                component = mount(
47
+                    <Component autoPlay />
48
+                );
49
+                testControl = component.find(TestControl);
50
+                expect(testControl.props()).toEqual({});
51
+            });
52
+
53
+            it('should be provided when a video event is triggered', () => {
54
+                component.find('video').node.dispatchEvent(new Event('play'));
55
+            });
56
+
57
+            it('should be provided when an error occurs on last source element', () => {
58
+                component.find('source').node.dispatchEvent(new Event('error'));
59
+            });
60
+
61
+            afterEach(() => {
62
+                // Only matching a subset is sufficient.
63
+                expect(testControl.props()).toMatchObject({
64
+                    controller: undefined,
65
+                    autoPlay: undefined,
66
+                    controls: false,
67
+                    currentSrc: '',
68
+                    currentTime: 0,
69
+                    defaultMuted: false,
70
+                    defaultPlaybackRate: 1,
71
+                    duration: 0,
72
+                    ended: false,
73
+                    error: undefined,
74
+                    loop: false,
75
+                    mediaGroup: undefined,
76
+                    muted: false,
77
+                    networkState: 0,
78
+                    paused: true,
79
+                    playbackRate: 1,
80
+                    preload: '',
81
+                    readyState: 0,
82
+                    seeking: false,
83
+                    src: '',
84
+                    startDate: undefined,
85
+                    volume: 1
86
+                });
87
+            });
88
+        });
89
+
90
+        it('should remove all event listeners from the video element when unmounted', () => {
91
+            const removeEventListenerSpy = jest.fn();
92
+            component = mount(
93
+                <Component autoPlay />
94
+            );
95
+            const updateState = component.instance().updateState;
96
+            component.find('video').node.removeEventListener = removeEventListenerSpy;
97
+            expect(removeEventListenerSpy).not.toHaveBeenCalled();
98
+            component.unmount();
99
+            EVENTS.forEach((event) => {
100
+                expect(removeEventListenerSpy).toHaveBeenCalledWith(event.toLowerCase(), updateState);
101
+            });
102
+        });
103
+
104
+        it('should remove "error" event listener from the source element when unmounted', () => {
105
+            const removeEventListenerSpy = jest.fn();
106
+            component = mount(
107
+                <Component autoPlay />
108
+            );
109
+            const updateState = component.instance().updateState;
110
+            component.find('source').node.removeEventListener = removeEventListenerSpy;
111
+            expect(removeEventListenerSpy).not.toHaveBeenCalled();
112
+            component.unmount();
113
+            expect(removeEventListenerSpy).toHaveBeenCalledWith('error', updateState);
114
+        });
115
+    });
116
+
117
+    describe('mapping to props', () => {
118
+        let videoEl = {};
119
+
120
+        beforeAll(() => {
121
+            component = shallow(
122
+                <Component autoPlay />
123
+            );
124
+            // Emulate videoEl being present
125
+            // e.g. componentDidMount fired.
126
+            component.instance().videoEl = videoEl;
127
+            component.instance().forceUpdate();
128
+        });
129
+
130
+        beforeEach(() => {
131
+            // Reset spy
132
+            videoEl.play = jest.fn();
133
+        });
134
+
135
+        it('returns a component with it\'s ownProps', () => {
136
+            expect(component.find(TestVideo).prop('autoPlay'))
137
+                .toBe(true);
138
+        });
139
+
140
+        it('returns a component with a videoEl prop', () => {
141
+            expect(component.find(TestVideo).prop('videoEl'))
142
+                .toBe(videoEl);
143
+        });
144
+
145
+        it('returns a component with all of its state on the `video` prop', () => {
146
+            const state = {
147
+                html5: '1',
148
+                dom: 2,
149
+                properties: function() {
150
+                    return 3;
151
+                }
152
+            };
153
+            component.setState(state);
154
+            expect(component.find(TestVideo).prop('video'))
155
+                .toEqual(state);
156
+        });
157
+
158
+        it('can customise the mapping of props using mapToProps', () => {
159
+            const Component = video(TestVideo, (state, ownProps) => {
160
+                return {
161
+                    state,
162
+                    ownProps
163
+                };
164
+            });
165
+            const component = shallow(
166
+                <Component autoPlay />
167
+            );
168
+            component.setState({
169
+                paused: true
170
+            });
171
+            expect(component.find(TestVideo).prop('state').paused)
172
+                .toBe(true);
173
+            expect(component.find(TestVideo).prop('ownProps').autoPlay)
174
+                .toBe(true);
175
+        });
176
+
177
+        it('can map videoEl to props for creating custom API methods', () => {
178
+            const Component = video(TestVideo, undefined, (el, state, ownProps) => {
179
+                return {
180
+                    togglePlay: () => {
181
+                        el.play(ownProps.testProp);
182
+                    }
183
+                }
184
+            });
185
+            const component = shallow(
186
+                <Component autoPlay testProp="testValue" />
187
+            );
188
+            component.instance().videoEl = videoEl;
189
+            component.instance().forceUpdate();
190
+            component.find(TestVideo).prop('togglePlay')();
191
+            expect(videoEl.play).toHaveBeenCalledWith('testValue');
192
+        });
193
+
194
+        it('allows mapVideoElToProps to take precedence over mapStateToProps', () => {
195
+            const Component = video(TestVideo, () => ({
196
+                duplicateKey: 'mapStateToProps'
197
+            }), () => ({
198
+                duplicateKey: 'mapVideoElToProps'
199
+            }));
200
+            const component = shallow(
201
+                <Component />
202
+            );
203
+            expect(component.find(TestVideo).prop('duplicateKey')).toBe('mapVideoElToProps');
204
+        });
205
+
206
+        it('allows ownProps to take precedence over mapVideoElToProps and mapStateToProps', () => {
207
+            const Component = video(TestVideo, () => ({
208
+                duplicateKey: 'mapStateToProps'
209
+            }), () => ({
210
+                duplicateKey: 'mapVideoElToProps'
211
+            }));
212
+            const component = shallow(
213
+                <Component duplicateKey="ownProps" />
214
+            );
215
+            expect(component.find(TestVideo).prop('duplicateKey')).toBe('ownProps');
216
+        });
217
+
218
+        it('allows cusomtisation of merging ownProps, mapVideoElToProps and mapStateToProps to change the merging precedence', () => {
219
+            const Component = video(TestVideo, () => ({
220
+                duplicateKey: 'mapStateToProps'
221
+            }), () => ({
222
+                duplicateKey: 'mapVideoElToProps'
223
+            }), (stateProps, videoElProps, ownProps) =>
224
+                Object.assign({}, ownProps, stateProps, videoElProps));
225
+            const component = shallow(
226
+                <Component duplicateKey="ownProps" />
227
+            );
228
+            expect(component.find(TestVideo).prop('duplicateKey')).toBe('mapVideoElToProps');
229
+        });
230
+    });
231
+});
232
+
233
+
234
+

+ 8
- 1
yarn.lock View File

@@ -8321,6 +8321,13 @@ react-app-polyfill@^1.0.5:
8321 8321
     regenerator-runtime "^0.13.3"
8322 8322
     whatwg-fetch "^3.0.0"
8323 8323
 
8324
+react-app-rewired@^2.1.5:
8325
+  version "2.1.5"
8326
+  resolved "https://registry.yarnpkg.com/react-app-rewired/-/react-app-rewired-2.1.5.tgz#592ec2eae5c3c5cd96c80930b5dc3f6c34da1dc6"
8327
+  integrity sha512-Gr8KfCeL9/PTQs8Vvxc7v8wQ9vCFMnYPhcAkrMlzkLiMFXS+BgSwm11MoERjZm7dpA2WjTi+Pvbu/w7rujAV+A==
8328
+  dependencies:
8329
+    semver "^5.6.0"
8330
+
8324 8331
 react-dev-utils@^10.0.0:
8325 8332
   version "10.0.0"
8326 8333
   resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.0.0.tgz#bd2d16426c7e4cbfed1b46fb9e2ac98ec06fcdfa"
@@ -8371,7 +8378,7 @@ react-is@^16.8.1, react-is@^16.8.4:
8371 8378
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
8372 8379
   integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
8373 8380
 
8374
-react-scripts@3.3.0:
8381
+react-scripts@^3.3.0:
8375 8382
   version "3.3.0"
8376 8383
   resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.3.0.tgz#f26a21f208f20bd04770f43e50b5bbc151920c2a"
8377 8384
   integrity sha512-hzPc6bxCc9GnsspWqk494c2Gpd0dRbk/C8q76BNQIENi9GMwoxFljOEcZoZcpFpJgQ45alxFR6QaLt+51qie7g==