diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..16951826 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +**/dist +**/es diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..fdb7a3d1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,62 @@ +const OFF = 0 +const ERROR = 2 + +module.exports = { + // parse proposal features like class fileds + parser: 'babel-eslint', + + // https://eslint.org/docs/user-guide/configuring#specifying-parser-options + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module', + ecmaFeatures: { + // Supports JSX syntax (not the same as supporting React). + jsx: true, + }, + }, + + // commonly used envs + env: { + browser: true, + node: true, + es6: true, + jest: true, + }, + + // we use recommended configurations + extends: [ + // https://eslint.org/docs/rules/ + 'eslint:recommended', + // https://github.com/yannickcr/eslint-plugin-react + 'plugin:react/recommended', + // https://github.com/benmosher/eslint-plugin-import + 'plugin:import/recommended', + // https://github.com/prettier/eslint-plugin-prettier + 'plugin:prettier/recommended', + ], + + plugins: ['react-hooks'], + + rules: { + 'prettier/prettier': ['error', require('./.prettierrc')], + + // disable nice-to-have rules for migrate convenience + 'react/prop-types': OFF, + 'react/no-find-dom-node': OFF, + 'react/display-name': OFF, + + // recommended rules + 'prefer-const': ERROR, + 'no-var': ERROR, + + 'react-hooks/rules-of-hooks': ERROR, + }, + + settings: { + // https://github.com/yannickcr/eslint-plugin-react#configuration + react: { + version: '16', + }, + 'import/core-modules': ['griffith', 'griffith-mp4'], + }, +} diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..2f0da580 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,30 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +--- + +## 🐛 Bug Report + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +## Expected behavior + +A clear and concise description of what you expected to happen. + +## Link to repl or repo (highly encouraged) + +Please provide a minimal repository on GitHub. + +Issues without a reproduction link are likely to stall. + +## Run `npx envinfo --system --binaries --npmPackages griffith --markdown --clipboard` + +Paste the results here: + +```bash + +``` diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..2f0da580 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,30 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +--- + +## 🐛 Bug Report + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +## Expected behavior + +A clear and concise description of what you expected to happen. + +## Link to repl or repo (highly encouraged) + +Please provide a minimal repository on GitHub. + +Issues without a reproduction link are likely to stall. + +## Run `npx envinfo --system --binaries --npmPackages griffith --markdown --clipboard` + +Paste the results here: + +```bash + +``` diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..620c9261 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,11 @@ +--- +name: 💬 Questions / Help +about: If you have questions, please read full readme first +--- + +## 💬 Questions and Help + +- Read carefully the README of the project +- Search if your answer has already been answered in old issues + +After you can submit your question and we will be happy to help you! diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bf7d7860 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +node_modules +/coverage + +bin + +dist +cjs +es + +# Files +package-lock.json +.eslintcache +*.log +.DS_Store diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..a99bdedb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +package.json +package-lock.json +lerna.json +CHANGELOG.md +**/dist +**/es diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..20f7ba77 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + semi: false, + singleQuote: true, + trailingComma: 'es5', +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..06deac65 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fe@zhihu.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING-zh_CN.md b/CONTRIBUTING-zh_CN.md new file mode 100644 index 00000000..9841c993 --- /dev/null +++ b/CONTRIBUTING-zh_CN.md @@ -0,0 +1,75 @@ +[English](./CONTRIBUTING.md) | 简体中文 + +# 贡献指南 + +这篇指南会指导你如何为 Griffith 贡献一份自己的力量,请在你要提 issue 或者 pull request 之前花几分钟来阅读一遍这篇指南。 + +## [行为准则](./CODE_OF_CONDUCT.md) + +我们有一份[行为准则](./CODE_OF_CONDUCT.md),希望所有的贡献者都能遵守,请花时间阅读一遍全文以确保你能明白哪些是可以做的,哪些是不可以做的。 + +## 透明的开发 + +我们所有的工作都会放在 GitHub 上。不管是核心团队的成员还是外部贡献者的 pull request 都需要经过同样流程的 review。 + +### 工作流和 Pull Request + +在提交 PR 前,请确保你做了以下的事情。 + +1. Fork Griffith 仓库,并在新仓库创建新的分支。这里有一份如何 fork 的指南:https://help.github.com/articles/fork-a-repo/。 + + 打开命令行: + + ```sh + $ git clone https://github.com//griffith + $ cd griffith + $ git checkout -b my_branch + ``` + + Note: 把 `` 替换成你的 GitHub 用户名。 + +2. Griffith 使用 [Yarn](https://yarnpkg.com/zh-Hant/) 来管理项目依赖. 请确保你的开发环境已经安装了 Yarn。 + +3. 使用 Yarn 安装依赖: + + ```sh + yarn + ``` + + 检查 yarn 版本 + + ```sh + yarn --version + ``` + +4. 我们提供了多个例子供开发使用,你可以直接运行以下脚本,根据 webpack 提示进行开发。 + + ```sh + yarn start + ``` + +5. 如果你修改了 APIs,请更新文档。 + +6. 确保能通过 lint 校验。 + + ```sh + npm run lint:fix + ``` + +7. 请确保能通过所有测试用例。 + + ```sh + yarn test + ``` + +8. 请确保 commit 符合规范。Griffith 使用[约定式提交](https://www.conventionalcommits.org/zh/v1.0.0-beta.2/) 来规范 commit。 + +## 提交 bug + +### 查找已知的 Issues + +我们使用 GitHub Issues 来管理项目 bug。 我们将密切关注已知 bug,并尽快修复。 在提交新问题之前,请尝试确保您的问题尚不存在。 + +### 提交新的 Issues + +请按照 Issues Template 的指示来提交新的 Issues。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..04299a66 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,79 @@ +English | [简体中文](./CONTRIBUTING-zh_CN.md) + +# How to Contribute + +The following is a set of guidelines for contributing to Griffith. Please spend several minutes in reading these guidelines before you create an issue or pull request. + +## [Code of Conduct](./CODE_OF_CONDUCT.md) + +We have adopted a Code of Conduct that we expect project participants to adhere to. Please read the [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. + +## Open Development + +All work on Griffith happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process. + +### Workflow and Pull Requests + +_Before_ submitting a pull request, please make sure the following is done… + +1. Fork the repo and create your branch from `master`. A guide on how to fork a repository: https://help.github.com/articles/fork-a-repo/ + + Open terminal (e.g. Terminal, iTerm, Git Bash or Git Shell) and type: + + ```sh + $ git clone https://github.com//griffith + $ cd griffith + $ git checkout -b my_branch + ``` + + Note: Replace `` with your GitHub username + +2. Griffith uses [Yarn](https://yarnpkg.com/en/) for running development scripts. If you haven't already done so, please [install yarn](https://yarnpkg.com/en/docs/install). + +3) Run `yarn install`. On Windows: To install [Yarn](https://yarnpkg.com/en/docs/install#windows-tab) on Windows you may need to download either node.js or Chocolatey
. + + ```sh + yarn + ``` + + To check your version of Yarn and ensure it's installed you can type: + + ```sh + yarn --version + ``` + +4) We have provided several examples for development. You can run the following script and develop according to the webpack prompt. + + ```sh + yarn start + ``` + +5) If you've changed APIs, update the documentation. + +6) Ensure the linting is good via `yarn run lint:fix`. + + ```sh + npm run lint:fix + ``` + +7) Ensure the testing is good via `yarn run test`. + + ```sh + yarn test + ``` + +8) Ensure the commit is readable. see [Conventional Commits](https://www.conventionalcommits.org/zh/v1.0.0-beta.2/). + +## Bugs + +### Where to Find Known Issues + +We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues + +The best way to get your bug fixed is to provide a reduced test case. Please provide a public repository with a runnable example. + +## License + +By contributing to Griffith, you agree that your contributions will be licensed under its MIT license. diff --git a/README-zh_CN.md b/README-zh_CN.md new file mode 100644 index 00000000..c261de75 --- /dev/null +++ b/README-zh_CN.md @@ -0,0 +1,128 @@ +

+ +

知乎视频播放器

+

+ +[English](./README.md) | 简体中文 + +## 简介 + +

+ +

+ +- **流式播放:** Griffith 让流式播放变得简单。无论你视频格式是 mp4 还是 hls,Griffith 都能使用媒体源拓展(MSE)来实现分段加载等功能。 +- **可扩展性:** Griffith 让 React 应用接入视频播放功能变得简单。如果你的应用不基于 React,Griffith 还提供支持 standalone 模式可以免构建工具直接在浏览器中使用。 +- **可靠性:** Griffith 已经大范围应用知乎的桌面和移动 web 应用中。 + +## 快速开始 + +### React 应用 + +```bash +$ yarn add griffith +``` + +```js +import Player from 'Griffith' + +const playlist = { + hd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, +} + +render() +``` + +[查看详细使用方法](./packages/griffith/README-zh_CN.md) + +**注意:暂不支持 SSR 应用** + +### 非 React 应用 + +```html + +``` + +```javascript +const playlist = { + hd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, +} + +Griffith.createPlayer(element).render({playlist}) +``` + +[查看 Standalone 模式详细使用方法](./packages/griffith-standalone/README-zh_CN.md) + +## 项目结构 + +Griffith 是一个 monorepo,使用了 [Yarn workspace](https://yarnpkg.com/lang/en/docs/workspaces/) 和 [Lerna](https://github.com/lerna/lerna) 进行管理。 + +### 核心 + +- `packages/griffith`: 核心代码 + +### 工具 + +- `packages/griffith-message`: 帮助进行跨窗口通信 +- `packages/griffith-utils`: 公用的工具函数 + +### 插件 + +- `packages/griffith-mp4`: MP4 插件。使用了 [MediaSource API](https://developer.mozilla.org/en-US/docs/Web/API/MediaSource) +- `packages/griffith-hls`: [HLS](https://developer.apple.com/streaming/) 插件,使用了 [hls.js](https://github.com/video-dev/hls.js) + +### 其他 + +- `example`: 示例 +- `packages/griffith-standalone`: 包含了所有依赖的 UMD 包,可以免除构建工具,独立在浏览器中使用。 + +## 自定义构建 + +默认情况下,webpack 等构建工具会将 `griffith-mp4` 和 `packages/griffith-hls` 打包。你可以通过构建时注入全局变量来除去插件,从而减小打包大小。 + +如果你使用 webpack,可以使用 [DefinePlugin](https://webpack.js.org/plugins/define-plugin/): + +```javascript +const {DefinePlugin} = require('webpack') + +module.exports = { + plugins: [ + new DefinePlugin({ + __WITHOUT_HLSJS__: JSON.stringify(true), // 除去 griffith-hls + __WITHOUT_MP4__: JSON.stringify(true), // 除去 griffith-mp4 + }), + ], +} +``` + +注意,除去 `griffith-mp4` / `griffith-hls` 之后,除非浏览器原生支持,否则 Griffith 不能播放 MP4 / HLS 视频。 + +## 贡献 + +阅读以下内容,了解如何参与改进 Griffith。 + +### 贡献指南 + +查看我们的[贡献指南](./CONTRIBUTING-zh_CN.md)来了解我们的开发流程。 + +### 贡献者 + +- Cheng Wang +- Wuhao Liu +- Xiaoyan Li +- Tianxiao Wang +- Xiaoshuang Bai (Designer) + +## 版权许可证 + +MIT diff --git a/README.md b/README.md new file mode 100644 index 00000000..4667aedf --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +

+ +

React-based web video player

+

+ +English | [简体中文](./README-zh_CN.md) + +# Introduction + +

+ +

+ +- **Streaming:** griffith makes streaming easy. Whether your video format is mp4 or hls, griffith can use Media Source Extension (MSE) for segment loading. +- **Extensibility:** griffith makes it simple to support video features in React apps. It also supports the UMD (Universal Module Definition) patterns for use directly in the browser if your application is not based on React. +- **Reliability:** griffith has been widely used in the web and mobile web of zhihu. + +## Usage + +### React application + +```bash +$ yarn add griffith +``` + +```js +import Player from 'Griffith' + +const playlist = { + hd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, +} + +render() +``` + +[Detailed usage](./packages/griffith/README.md) + +**Note: SSR application is not supported** + +### non-React application + +```html + +``` + +```js +const playlist = { + hd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, +} + +Griffith.createPlayer(element).render({playlist}) +``` + +[Standalone mode detailed usage](./packages/griffith-standalone/README.md) + +## Project Structure + +Griffith is a mono-repo and use [Yarn workspace](https://yarnpkg.com/lang/en/docs/workspaces/) and [monorepo](https://github.com/lerna/lerna). + +### Core + +- `packages/griffith`: The core lirary + +### Utilities + +- `packages/griffith-message`: Helpers for cross-window message +- `packages/griffith-utils`: Utilities + +### Plugins + +- `packages/griffith-mp4`: MP4 plugin powered by [MediaSource API](https://developer.mozilla.org/en-US/docs/Web/API/MediaSource) +- `packages/griffith-hls`: [HLS](https://developer.apple.com/streaming/) plugin powered by [hls.js](https://github.com/video-dev/hls.js) + +### Others + +- `example`: example and demos +- `packages/griffith-standalone`: A UMD build that can be used without React or Webpack + +## Custom Build + +Build tools like webpack will include `griffith-mp4` and `packages/griffith-hls` by default. You can make your bundle smaller by exluding a plugin with build-time globals. + +If you use webpack, do so with [DefinePlugin](https://webpack.js.org/plugins/define-plugin/): + +```javascript +const {DefinePlugin} = require('webpack') + +module.exports = { + plugins: [ + new DefinePlugin({ + __WITHOUT_HLSJS__: JSON.stringify(true), // exludes griffith-hls + __WITHOUT_MP4__: JSON.stringify(true), // exludes griffith-mp4 + }), + ], +} +``` + +Note that without `griffith-mp4` / `griffith-hls` Griffith can no longer play MP4 / HLS media unless the browser supports it natively. + +## Contributing + +Read below to learn how you can take part in improving griffith. + +### [Contributing Guide](./CONTRIBUTING.md) + +Read our [contributing guide](./CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to griffith. + +### Contributor + +- Cheng Wang +- Wuhao Liu +- Xiaoyan Li +- Tianxiao Wang +- Xiaoshuang Bai (Designer) + +## LICENSE + +MIT diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..d13dd975 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,10 @@ +module.exports = api => { + const config = + api.env() === 'cjs' + ? {plugins: ['@babel/plugin-transform-modules-commonjs']} + : {presets: ['@zhihu/babel-preset/library']} + return { + babelrcRoots: ['.', './packages/*'], + ...config, + } +} diff --git a/example/babel.config.js b/example/babel.config.js new file mode 100644 index 00000000..0b41320d --- /dev/null +++ b/example/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@zhihu/babel-preset/app'], + plugins: ['react-hot-loader/babel'], +} diff --git a/example/fmp4/App.js b/example/fmp4/App.js new file mode 100644 index 00000000..145efd34 --- /dev/null +++ b/example/fmp4/App.js @@ -0,0 +1,42 @@ +import React from 'react' +import {hot} from 'react-hot-loader' +import PlayerContainer from 'griffith' + +const duration = 182 + +const sources = { + hd: { + bitrate: 2005, + size: 46723282, + duration, + format: 'mp4', + width: 1280, + height: 720, + play_url: 'mp4/zhihu2018_hd.mp4', + }, + sd: { + bitrate: 900.49, + size: 20633151, + duration, + format: 'mp4', + width: 320, + height: 240, + play_url: 'mp4/zhihu2018_sd.mp4', + }, +} + +const props = { + id: 'zhihu2018', + standalone: true, + title: '2018 我们如何与世界相处?', + cover: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018.jpg', + duration, + sources, + shouldObserveResize: true, + src: 'mp4/zhihu2018_sd.mp4', + useMSE: true, +} + +const App = () => + +export default hot(module)(App) diff --git a/example/fmp4/index.html b/example/fmp4/index.html new file mode 100644 index 00000000..a439c9e7 --- /dev/null +++ b/example/fmp4/index.html @@ -0,0 +1,33 @@ + + + + + + Zhihu Video Player + + + + +
+ + diff --git a/example/fmp4/index.js b/example/fmp4/index.js new file mode 100644 index 00000000..259c5289 --- /dev/null +++ b/example/fmp4/index.js @@ -0,0 +1,10 @@ +/* eslint-disable import/named */ +import '@babel/polyfill' +import 'raf/polyfill' +import 'whatwg-fetch' +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +const target = document.getElementById('player') +ReactDOM.render(, target) diff --git a/example/hls/App.js b/example/hls/App.js new file mode 100644 index 00000000..b76dd424 --- /dev/null +++ b/example/hls/App.js @@ -0,0 +1,52 @@ +import React from 'react' +import {hot} from 'react-hot-loader' +import PlayerContainer from 'griffith' + +const duration = 89 + +const sources = { + hd: { + bitrate: 905, + size: 10105235, + duration, + format: 'm3u8', + width: 640, + height: 480, + play_url: + 'http://zhihu-video-output.oss-cn-hangzhou.aliyuncs.com/test/hd-m3u8/999f95fc-0346-11e9-b494-0a580a44d740.m3u8', + }, + sd: { + bitrate: 580, + size: 6531802, + duration, + format: 'm3u8', + width: 320, + height: 240, + play_url: + 'http://zhihu-video-output.oss-cn-hangzhou.aliyuncs.com/test/sd-m3u8/999f95fc-0346-11e9-b494-0a580a44d740.m3u8', + }, + ld: { + bitrate: 261, + size: 2984172, + duration, + format: 'm3u8', + width: 160, + height: 120, + play_url: + 'http://zhihu-video-output.oss-cn-hangzhou.aliyuncs.com/test/ld-m3u8/999f95fc-0346-11e9-b494-0a580a44d740.m3u8', + }, +} + +const props = { + id: 'zhihu2018', + title: '2018 我们如何与世界相处?', + standalone: true, + cover: 'https://zhstatic.zhihu.com/cfe/griffith/player.png', + duration, + sources, + shouldObserveResize: true, +} + +const App = () => + +export default hot(module)(App) diff --git a/example/hls/index.html b/example/hls/index.html new file mode 100644 index 00000000..a439c9e7 --- /dev/null +++ b/example/hls/index.html @@ -0,0 +1,33 @@ + + + + + + Zhihu Video Player + + + + +
+ + diff --git a/example/hls/index.js b/example/hls/index.js new file mode 100644 index 00000000..259c5289 --- /dev/null +++ b/example/hls/index.js @@ -0,0 +1,10 @@ +/* eslint-disable import/named */ +import '@babel/polyfill' +import 'raf/polyfill' +import 'whatwg-fetch' +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +const target = document.getElementById('player') +ReactDOM.render(, target) diff --git a/example/iframe/index.html b/example/iframe/index.html new file mode 100644 index 00000000..ad89e13c --- /dev/null +++ b/example/iframe/index.html @@ -0,0 +1,28 @@ + + + + + + + Document + + + +

本页面可以测试播放器在 iframe 中的效果,还可以测试跨窗口消息接口

+

场景 1:向一个视频发出暂停指令

+

场景 2:一个视频开始播放时,暂停其他视频

+
+ + + + +
+
+ + + diff --git a/example/iframe/index.js b/example/iframe/index.js new file mode 100644 index 00000000..a5ec2876 --- /dev/null +++ b/example/iframe/index.js @@ -0,0 +1,22 @@ +import {EVENTS, ACTIONS, createMessageHelper} from 'griffith-message' + +const {subscribeMessage, dispatchMessage} = createMessageHelper() + +function pauseAllOtherVideos(thisWindow) { + Array.from(document.querySelectorAll('iframe')) + .map(node => node.contentWindow) + .filter(w => w !== thisWindow) + .forEach(w => dispatchMessage(w, ACTIONS.PLAYER.PAUSE)) +} + +subscribeMessage((messageName, data, sourceWindow) => { + if (messageName === EVENTS.DOM.PLAY) { + pauseAllOtherVideos(sourceWindow) + } +}) + +const firstVideoWindow = document.getElementById('video-1').contentWindow + +document.getElementById('button').addEventListener('click', () => { + dispatchMessage(firstVideoWindow, ACTIONS.PLAYER.PAUSE) +}) diff --git a/example/inline/App.js b/example/inline/App.js new file mode 100644 index 00000000..1f6f0c48 --- /dev/null +++ b/example/inline/App.js @@ -0,0 +1,97 @@ +import React from 'react' +import {hot} from 'react-hot-loader' +import PlayerContainer, {Layer} from 'griffith' + +const watermarkStyle = { + backgroundColor: 'rgba(255, 255, 255, 0.5)', + color: 'white', + borderRadius: '5px', + margin: '5px', + padding: '5px', + display: 'inline-block', +} + +const VideoCard = ({data, height = 'auto', objectFit}) => ( +
+ + + 水印示例 + + +
+) + +class App extends React.Component { + render() { + const duration = 182 + + const sources = { + hd: { + bitrate: 2005, + size: 46723282, + duration, + format: 'fmp4', + width: 1280, + height: 720, + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + bitrate: 900.49, + size: 20633151, + duration, + format: 'fmp4', + width: 848, + height: 478, + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, + } + + const data = { + id: 'zhihu2018', + title: '2018 我们如何与世界相处?', + cover: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018.jpg', + duration, + sources, + src: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + } + + return ( + +

行内视频示例

+

视频原始比例为 16:9

+

暂时只支持 contain 和 cover 两种情况

+
+
+

正方形播放器

+

object-fit: contain (default)

+

预期:视频上下黑边;水印相对视频画面定位

+ +

object-fit: cover

+

预期:视频左右裁切;水印相对播放器定位

+ +
+
+
+

2:1 播放器

+

object-fit: contain (default)

+

预期:视频左右黑边;水印相对视频画面定位

+ +

object-fit: cover

+

预期:视频上下裁切;水印相对播放器定位

+ +
+
+
+

不指定高度

+

预期:高度自适应

+ +
+
+ ) + } +} + +export default hot(module)(App) diff --git a/example/inline/index.html b/example/inline/index.html new file mode 100644 index 00000000..92e5dfd8 --- /dev/null +++ b/example/inline/index.html @@ -0,0 +1,21 @@ + + + + + + Inline 示例 + + + + +
+ + diff --git a/example/inline/index.js b/example/inline/index.js new file mode 100644 index 00000000..259c5289 --- /dev/null +++ b/example/inline/index.js @@ -0,0 +1,10 @@ +/* eslint-disable import/named */ +import '@babel/polyfill' +import 'raf/polyfill' +import 'whatwg-fetch' +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +const target = document.getElementById('player') +ReactDOM.render(, target) diff --git a/example/main/App.js b/example/main/App.js new file mode 100644 index 00000000..20df50f7 --- /dev/null +++ b/example/main/App.js @@ -0,0 +1,14 @@ +import React from 'react' +import {hot} from 'react-hot-loader' + +const App = () => ( + +) + +export default hot(module)(App) diff --git a/example/main/index.html b/example/main/index.html new file mode 100644 index 00000000..dee34acb --- /dev/null +++ b/example/main/index.html @@ -0,0 +1,37 @@ + + + + + + Zhihu Video Player + + + + +
+ + diff --git a/example/main/index.js b/example/main/index.js new file mode 100644 index 00000000..259c5289 --- /dev/null +++ b/example/main/index.js @@ -0,0 +1,10 @@ +/* eslint-disable import/named */ +import '@babel/polyfill' +import 'raf/polyfill' +import 'whatwg-fetch' +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +const target = document.getElementById('player') +ReactDOM.render(, target) diff --git a/example/mp4/App.js b/example/mp4/App.js new file mode 100644 index 00000000..80c2ea84 --- /dev/null +++ b/example/mp4/App.js @@ -0,0 +1,48 @@ +import React from 'react' +import {hot} from 'react-hot-loader' +import PlayerContainer, {MessageContext} from 'griffith' +import LayerTest from './LayerTest' + +const duration = 182 + +const sources = { + hd: { + bitrate: 2005, + size: 46723282, + duration, + format: 'mp4', + width: 1280, + height: 720, + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_hd.mp4', + }, + sd: { + bitrate: 900.49, + size: 20633151, + duration, + format: 'mp4', + width: 320, + height: 240, + play_url: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', + }, +} + +const props = { + id: 'zhihu2018', + standalone: true, + title: '2018 我们如何与世界相处?', + cover: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018.jpg', + duration, + sources, + shouldObserveResize: true, + src: 'https://zhstatic.zhihu.com/cfe/griffith/zhihu2018_sd.mp4', +} + +const App = () => ( + + + {({subscribeEvent}) => } + + +) + +export default hot(module)(App) diff --git a/example/mp4/LayerTest.js b/example/mp4/LayerTest.js new file mode 100644 index 00000000..9cee54b4 --- /dev/null +++ b/example/mp4/LayerTest.js @@ -0,0 +1,54 @@ +import React, {Component} from 'react' +import {parse} from 'query-string' +import {StyleSheet, css} from 'aphrodite/no-important' +import {Layer} from 'griffith' +import {EVENTS} from 'griffith-message' + +const styles = StyleSheet.create({ + logo: { + width: '20%', + position: 'absolute', + top: '3%', + right: '4%', + }, +}) + +const LOGO_SRC = 'http://zhstatic.zhihu.com/assets/zhihu/web-logo@2x.png' + +class LayerTest extends Component { + state = { + shouldShow: false, + } + + componentDidMount() { + const {logo} = parse(location.search) + if (logo) { + this.subscription = this.props.subscribeEvent( + EVENTS.PLAYER.PLAY_COUNT, + () => { + this.setState({shouldShow: true}) + } + ) + } + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe() + } + } + + render() { + const {shouldShow} = this.state + + return ( + shouldShow && ( + + + + ) + ) + } +} + +export default LayerTest diff --git a/example/mp4/index.html b/example/mp4/index.html new file mode 100644 index 00000000..a439c9e7 --- /dev/null +++ b/example/mp4/index.html @@ -0,0 +1,33 @@ + + + + + + Zhihu Video Player + + + + +
+ + diff --git a/example/mp4/index.js b/example/mp4/index.js new file mode 100644 index 00000000..259c5289 --- /dev/null +++ b/example/mp4/index.js @@ -0,0 +1,10 @@ +/* eslint-disable import/named */ +import '@babel/polyfill' +import 'raf/polyfill' +import 'whatwg-fetch' +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +const target = document.getElementById('player') +ReactDOM.render(, target) diff --git a/example/package.json b/example/package.json new file mode 100644 index 00000000..b25ff731 --- /dev/null +++ b/example/package.json @@ -0,0 +1,21 @@ +{ + "name": "example", + "version": "1.0.1", + "private": true, + "scripts": { + "build": "NODE_ENV=production webpack --env production", + "start": "webpack-dev-server --hot" + }, + "dependencies": { + "@babel/polyfill": "^7.0.0", + "@babel/runtime": "^7.1.5", + "griffith": "^1.0.1", + "hls.js": "^0.12.4", + "query-string": "^6.3.0", + "raf": "^3.4.1", + "react": "16.4.1", + "react-dom": "16.4.1", + "react-hot-loader": "^4.3.12", + "whatwg-fetch": "^3.0.0" + } +} diff --git a/example/webpack.config.js b/example/webpack.config.js new file mode 100644 index 00000000..99f2a9dc --- /dev/null +++ b/example/webpack.config.js @@ -0,0 +1,117 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin') + +module.exports = env => { + const isProduction = env === 'production' + + return { + mode: isProduction ? 'production' : 'development', + + entry: { + main: './main/index.js', + iframe: './iframe/index.js', + inline: './inline/index.js', + fmp4: './fmp4/index.js', + hls: './hls/index.js', + mp4: './mp4/index.js', + }, + + devServer: { + disableHostCheck: true, + port: 8000, + proxy: { + '/mp4': { + target: 'https://zhstatic.zhihu.com/cfe/griffith/', + pathRewrite: {'^/mp4' : ''}, + changeOrigin: true, + secure: false, + }, + } + }, + + devtool: 'cheap-module-eval-source-map', + + resolve: { + alias: { + griffith: 'griffith/src', + 'griffith-mp4': 'griffith-mp4/src', + 'griffith-hls': 'griffith-hls/src', + }, + }, + + module: { + rules: [ + { + test: /\.js$/, + oneOf: [ + { + exclude: /node_modules/, + loader: 'babel-loader', + options: { + cacheDirectory: true, + }, + }, + { + exclude: /@babel\/runtime/, + loader: 'babel-loader', + options: { + cacheDirectory: true, + configFile: false, + presets: ['@zhihu/babel-preset/dependencies'], + compact: false, + }, + }, + ], + }, + ], + }, + + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: 'initial', + name: 'vendor', + test: /[\\/]node_modules[\\/]/, + }, + common: { + chunks: 'initial', + name: 'common', + test: /packages\/griffith/, + }, + }, + }, + }, + + plugins: [ + new HtmlWebpackPlugin({ + template: './main/index.html', + chunks: ['main', 'common', 'vendor'], + }), + new HtmlWebpackPlugin({ + template: './iframe/index.html', + filename: 'iframe.html', + chunks: ['iframe', 'common', 'vendor'], + }), + new HtmlWebpackPlugin({ + template: './inline/index.html', + filename: 'inline.html', + chunks: ['inline', 'common', 'vendor'], + }), + new HtmlWebpackPlugin({ + template: './fmp4/index.html', + filename: 'fmp4.html', + chunks: ['fmp4', 'common', 'vendor'], + }), + new HtmlWebpackPlugin({ + template: './hls/index.html', + filename: 'hls.html', + chunks: ['hls', 'common', 'vendor'], + }), + new HtmlWebpackPlugin({ + template: './mp4/index.html', + filename: 'mp4.html', + chunks: ['mp4', 'common', 'vendor'], + }), + ], + } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..3c69cbda --- /dev/null +++ b/jest.config.js @@ -0,0 +1,26 @@ +module.exports = { + coverageReporters: ['html', 'cobertura', 'lcov'], + rootDir: process.cwd(), + roots: ['/packages'], + collectCoverageFrom: ['packages/**/src/**/*.js'], + coveragePathIgnorePatterns: [ + 'webpack.config.js', + '.babelrc.js', + 'constants', + 'index.js', + 'styles.js', + 'constants.js', + 'icons', + ], + testPathIgnorePatterns: [ + '/node_modules/', + '/packages/[^/]+?/(?!src/)', + '[^/]+?/__mocks__', + ], + transform: { + '\\.js$': '@zhihu/babel-preset/jest', + }, + transformIgnorePatterns: ['/node_modules/@babel/runtime/'], + setupFiles: ['./jest.setup.js'], + snapshotSerializers: ['enzyme-to-json/serializer'], +} diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..2361f5f7 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,9 @@ +import Enzyme from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' +import * as Aphrodite from 'aphrodite' +import * as AphroditeNoImportant from 'aphrodite/no-important' + +Aphrodite.StyleSheetTestUtils.suppressStyleInjection() +AphroditeNoImportant.StyleSheetTestUtils.suppressStyleInjection() + +Enzyme.configure({adapter: new Adapter()}) diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..c8744cb2 --- /dev/null +++ b/lerna.json @@ -0,0 +1,8 @@ +{ + "packages": [ + "packages/*" + ], + "version": "1.0.1", + "npmClient": "yarn", + "useWorkspaces": true +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..17f42dd7 --- /dev/null +++ b/package.json @@ -0,0 +1,78 @@ +{ + "name": "griffith", + "description": "Zhihu Video Player", + "homepage": "https://github.com/zhihu/griffith#readme", + "repository": { + "type": "git", + "url": "https://github.com/zhihu/griffith.git" + }, + "private": true, + "license": "MIT", + "workspaces": [ + "packages/*", + "example" + ], + "scripts": { + "format": "prettier --write \"packages/**/*.{js,json,md}\" \"*.{js,json,md}\"", + "lint": "eslint --fix \"packages/**/*.js\" \"*.js\"", + "test": "jest", + "test:coverage": "jest --coverage", + "test:watch": "jest --watch --notify", + "clean": "lerna run clean", + "build:es": "lerna run build:es --parallel --ignore example --ignore griffith-standalone", + "build:cjs": "lerna run build:cjs --parallel --ignore example --ignore griffith-standalone", + "build:standalone": "yarn workspace griffith-standalone run build", + "build": "yarn run clean && yarn run build:es && yarn run build:cjs && yarn run build:standalone", + "start": "yarn workspace example run start" + }, + "devDependencies": { + "@babel/cli": "^7.1.5", + "@babel/core": "^7.1.6", + "@commitlint/config-conventional": "^7.3.1", + "@zhihu/babel-preset": "^1.6.0", + "babel-eslint": "^10.0.1", + "babel-jest": "^24.1.0", + "babel-loader": "^8.0.4", + "commitlint": "^7.4.0", + "enzyme": "^3.9.0", + "enzyme-adapter-react-16": "^1.10.0", + "enzyme-to-json": "^3.3.5", + "eslint": "^5.12.0", + "eslint-config-prettier": "^3.3.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-prettier": "^3.0.0", + "eslint-plugin-react": "^7.11.1", + "eslint-plugin-react-hooks": "^1.0.2", + "html-webpack-plugin": "^3.2.0", + "husky": "^1.3.1", + "jest": "^24.1.0", + "lerna": "^3.4.3", + "lint-staged": "^8.1.0", + "prettier": "^1.16.4", + "webpack": "^4.26.1", + "webpack-cli": "^3.1.2", + "webpack-dev-server": "^3.1.10" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "lint-staged": { + "*.{json,md,css}": [ + "prettier --write", + "git add" + ], + "*.{js,jsx}": [ + "prettier --write", + "eslint --fix", + "git add" + ] + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + } +} diff --git a/packages/griffith-hls/package.json b/packages/griffith-hls/package.json new file mode 100644 index 00000000..af4c30f1 --- /dev/null +++ b/packages/griffith-hls/package.json @@ -0,0 +1,24 @@ +{ + "name": "griffith-hls", + "version": "1.0.1", + "description": "griffith hls plugin", + "homepage": "https://github.com/zhihu/griffith/packages/griffith-hls", + "license": "MIT", + "files": [ + "cjs", + "es", + "src" + ], + "main": "es/index.js", + "module": "es/index.js", + "scripts": { + "clean": "rm -rf es cjs", + "build:es": "NODE_ENV=production babel src --root-mode upward -d es --ignore 'src/**/*.spec.js','src/**/__tests__'", + "build:cjs": "BABEL_ENV=cjs NODE_ENV=production babel es --root-mode upward -d cjs" + }, + "peerDependencies": { + "hls.js": ">=0.7.11 <1.0.0", + "react": ">=16.3.0 <17.0.0", + "react-dom": ">=16.3.0 <17.0.0" + } +} diff --git a/packages/griffith-hls/src/Video.js b/packages/griffith-hls/src/Video.js new file mode 100644 index 00000000..eb33b521 --- /dev/null +++ b/packages/griffith-hls/src/Video.js @@ -0,0 +1,58 @@ +import React, {Component} from 'react' +import Hls from 'hls.js/dist/hls.light.min' +import {getMasterM3U8Blob} from './utils' + +export default class Video extends Component { + hasLoadStarted = false + + componentDidMount() { + const {autoStartLoad = false} = this.props + this.hls = new Hls({ + autoStartLoad, + }) + this.hls.attachMedia(this.video) + const {sources} = this.props + const master = getMasterM3U8Blob(sources) + this.src = URL.createObjectURL(master) + this.hls.loadSource(this.src) + } + + componentDidUpdate(prevProps) { + const {currentQuality, sources, paused} = this.props + if (currentQuality !== prevProps.currentQuality) { + const source = sources.find(source => source.quality === currentQuality) + if (source) { + const levels = this.hls.levels + const level = levels.findIndex(item => item.url.includes(source.source)) + this.hls.nextLevel = level + } else { + this.hls.nextLevel = -1 + } + } + + if (!paused && prevProps.paused && !this.hasLoadStarted) { + this.hls.startLoad() + this.hasLoadStarted = true + } + } + + componentWillUnmount() { + this.hls.destroy() + } + + render() { + // eslint-disable-next-line + const {onRef, currentQuality, src, paused, ...props} = this.props + return ( +