├── .editorconfig
├── .eslintignore
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTE.md
├── FAQ.md
├── README.md
├── config
├── paths.js
├── webpack.config.demo.js
├── webpack.config.dev.js
└── webpack.config.umd.js
├── demo
├── public
│ ├── .gitignore
│ ├── fragment.json
│ ├── index.html
│ ├── logo.png
│ ├── subtitle-en.vtt
│ ├── subtitle-zh-cn.vtt
│ ├── thumbnail.vtt
│ └── umd.html
└── src
│ ├── container.jsx
│ ├── controller
│ └── main.js
│ ├── index.jsx
│ ├── style
│ ├── css
│ │ ├── bootstrap.css
│ │ └── layout-main.less
│ └── img
│ │ └── logo.svg
│ └── view
│ ├── layout
│ └── main
│ │ └── index.jsx
│ └── main
│ ├── basic
│ └── index.jsx
│ ├── custom
│ ├── custom.jsx
│ └── index.jsx
│ ├── flv
│ └── index.jsx
│ ├── fragment
│ └── index.jsx
│ ├── history
│ └── index.jsx
│ ├── hls
│ └── index.jsx
│ ├── index
│ └── index.jsx
│ ├── map
│ └── index.jsx
│ ├── playlist
│ └── index.jsx
│ ├── subtitle
│ └── index.jsx
│ └── thumbnail
│ └── index.jsx
├── karma.conf.js
├── package.json
├── scripts
├── build-demo.js
├── build-umd.js
├── read-and-wirte-version.js
└── start.js
├── src
├── api
│ ├── api.js
│ ├── flvjs-api.js
│ ├── hlsjs-api.js
│ └── html5-api.js
├── assets
│ ├── css
│ │ ├── _const.less
│ │ ├── _mixin.less
│ │ ├── animate.less
│ │ └── common.less
│ ├── icon
│ │ ├── demo.css
│ │ ├── demo_fontclass.html
│ │ ├── demo_symbol.html
│ │ ├── demo_unicode.html
│ │ ├── iconfont.css
│ │ ├── iconfont.eot
│ │ ├── iconfont.js
│ │ ├── iconfont.svg
│ │ ├── iconfont.ttf
│ │ └── iconfont.woff
│ └── img
│ │ └── logo.svg
├── history.jsx
├── i18n
│ ├── default.js
│ └── zh_CN.js
├── index.jsx
├── libs
│ ├── fetch
│ │ ├── config.js
│ │ └── index.js
│ ├── provider
│ │ ├── redux-provider.jsx
│ │ ├── saga-model-provider.jsx
│ │ └── saga-model.js
│ └── query-string
│ │ ├── decode-uri-component.js
│ │ └── index.js
├── loader.js
├── model-list.js
├── model
│ ├── controlbar.js
│ ├── end.js
│ ├── error-message.js
│ ├── fragment.js
│ ├── fullscreen.js
│ ├── history.js
│ ├── living.js
│ ├── loading.js
│ ├── muted.js
│ ├── not-autoplay.js
│ ├── picture-quality.js
│ ├── play-pause.js
│ ├── playback-rate.js
│ ├── ready.js
│ ├── rotate.js
│ ├── selection.js
│ ├── time-slider.js
│ ├── time.js
│ ├── track.js
│ ├── video
│ │ ├── events
│ │ │ ├── hlsjs.js
│ │ │ └── index.js
│ │ ├── index.js
│ │ └── outside-api.js
│ └── volume.js
├── playlist.jsx
├── provider.jsx
├── style.js
├── umd.jsx
├── utils
│ ├── browser.js
│ ├── const.js
│ ├── dom
│ │ ├── addEventListener.js
│ │ ├── contains.js
│ │ └── fullscreen.js
│ ├── error-code.js
│ ├── event.js
│ ├── fetch.js
│ ├── logger
│ │ └── index.js
│ ├── srt.js
│ ├── storage.js
│ ├── util.js
│ └── video.js
├── version.js
└── view
│ ├── components
│ ├── carousel.jsx
│ ├── contextmenu.jsx
│ ├── slider.jsx
│ └── tooltip.jsx
│ ├── contextmenu.jsx
│ ├── controlbar
│ ├── capture.jsx
│ ├── full-off-screen.jsx
│ ├── index.jsx
│ ├── next.jsx
│ ├── picture-quality.jsx
│ ├── play-pause.jsx
│ ├── playback-rate.jsx
│ ├── prev.jsx
│ ├── rotate.jsx
│ ├── setting
│ │ ├── index.jsx
│ │ ├── list.jsx
│ │ ├── picture-quality-list.jsx
│ │ ├── playback-rate-list.jsx
│ │ └── subtitle-list.jsx
│ ├── subtitle-select.jsx
│ ├── time-container.jsx
│ ├── time-slider
│ │ ├── index.jsx
│ │ └── selection.jsx
│ ├── time-tooltip.jsx
│ └── volume.jsx
│ ├── decorator
│ └── clear.js
│ ├── end.jsx
│ ├── error-message.jsx
│ ├── fragment.jsx
│ ├── history
│ ├── index.jsx
│ └── time-slider
│ │ ├── index.jsx
│ │ ├── selection.jsx
│ │ └── time-tooltip.jsx
│ ├── index.jsx
│ ├── loading.jsx
│ ├── not-autoplay.jsx
│ ├── title.jsx
│ └── track
│ └── subtitle.jsx
└── tests
├── assets
└── logo.png
├── events
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── model
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ ├── model
│ └── video.js
│ └── outside-api.js
├── playlist
└── unit
│ ├── index.jsx
│ └── model
│ └── video.js
├── props-2
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.contextMenu-element
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.contextMenu-false
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.fragment-object
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.fragment-string
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.logo-element
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.logo-string
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props.tracks
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── props
├── components
│ └── player.jsx
└── unit
│ ├── index.js
│ └── model
│ └── video.js
├── start.spec.js
└── util.js
/.editorconfig:
--------------------------------------------------------------------------------
1 |
2 | root = true
3 |
4 | [*]
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | .cache
4 | node_modules/
5 | /build/
6 | /demo/build/
7 | /dist/
8 | /libs/
9 | /es/
10 | /coverage/
11 | /src/assets/css/style.css
12 | /src/assets/icon
13 | /src/libs/flv/flv.min.js
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build.tar.gz
2 | .idea/
3 | .vscode/
4 | .cache
5 | node_modules/
6 | /build/
7 | /demo/build/
8 | /dist
9 | demo-build/
10 | /libs/
11 | /es/
12 | .DS_Store
13 | *.tgz
14 | *.swp
15 | *.swo
16 | package-lock.json
17 | lerna-debug.log
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 | console_html.tar.gz
22 | /console/
23 | /coverage/
24 | TESTS-*.xml
25 | /src/assets/css/style.css
26 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | build.tar.gz
2 | .idea/
3 | .vscode/
4 | node_modules/
5 | src/
6 | build/
7 | dist/
8 | demo-build/
9 | /demo/
10 | .DS_Store
11 | *.tgz
12 | *.swp
13 | *.swo
14 | lerna-debug.log
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 | /coverage/
19 | TESTS-*.xml
20 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | .cache
4 | node_modules/
5 | /build/
6 | /demo/build/
7 | /dist/
8 | /libs/
9 | /es/
10 | /coverage/
11 | /src/assets/css/style.css
12 | /src/assets/icon
13 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": es5,
3 | "printWidth": 80,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: osx
2 | sudo: required
3 | addons:
4 | #chrome: stable
5 | #启用火狐
6 | firefox: "58.0.2"
7 | language: node_js
8 | node_js:
9 | - "8.4.0"
10 | script: "npm run build"
11 | after_success:
12 | - bash <(curl -s https://codecov.io/bash)
13 |
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | 问题记录和说明。
4 |
5 | ### HLS直播经常卡住然后重连?
6 |
7 | 这种情况很可能是网络问题,网络问题有本地网络和不同运营商访问的问题,例如电信访问移动视频服务器,然而移动做了限流,下载ts会慢,导致服务端生成m3u8的文件已被覆盖。
8 |
9 | 这种问题怎么判别?
10 |
11 | 需要看m3u8文件`#EXT-X-TARGETDURATION`的大小,然后看任意ts下载的最大时间,如果超过`#EXT-X-TARGETDURATION`那么基本都会出现卡住然后重连的问题。这种网络问题会导致请求的m3u8文件中的`#EXT-X-MEDIA-SEQUENCE`字段不是逐渐递增的,会出现跳跃递增。
12 | 网络抖动也可能导致这个问题,**反正只要ts下载时间大于`#EXT-X-TARGETDURATION`就会容易出现这个问题**。
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const util = require('react-boilerplate-app-utils');
4 | const scriptsPackagename = util.scriptsPackagename;
5 | const path = require('path');
6 | const cwdPackageJsonConfig = util.getDefaultCwdPackageJsonConfig(
7 | scriptsPackagename
8 | );
9 |
10 | function pathResolve(relativePath) {
11 | return util.pathResolve(relativePath, scriptsPackagename);
12 | }
13 | //pathResolve使用相对路径,不要使用绝对路径
14 | var paths = {
15 | webpackDevConfig: pathResolve('config/webpack.config.dev.js'),
16 | webpackProdConfig: pathResolve('config/webpack.config.prod.js'),
17 | webpackDllConfig: pathResolve('config/webpack.config.dll.js'),
18 | //app 程序入口js文件
19 | appEntry: pathResolve(cwdPackageJsonConfig.appEntryPath),
20 | //dev server静态资源访问目录
21 | appPublic: pathResolve(cwdPackageJsonConfig.appPublicPath),
22 | //app 入口html文件
23 | appHtml: path.resolve(
24 | process.cwd(),
25 | cwdPackageJsonConfig.appPublicPath,
26 | cwdPackageJsonConfig.index
27 | ),
28 | //程序打包目录,根据prefixURL变化
29 | appBuild: path.join(
30 | process.cwd(),
31 | 'demo/build',
32 | cwdPackageJsonConfig.basename
33 | ),
34 | umdBuild: path.resolve(process.cwd(), 'dist'),
35 | //app 程序目录
36 | src: path.resolve(cwdPackageJsonConfig.appSrcPath),
37 | };
38 |
39 | module.exports = paths;
40 |
--------------------------------------------------------------------------------
/demo/public/.gitignore:
--------------------------------------------------------------------------------
1 | app-manifest.json
2 | dll-compare.json
3 | dll.app.js
4 | dll.app.js.map
5 |
--------------------------------------------------------------------------------
/demo/public/fragment.json:
--------------------------------------------------------------------------------
1 | {
2 | "total": {
3 | "begin": "2017-10-03 00:00:00",
4 | "end": "2017-10-03 00:01:19"
5 | },
6 | "fragments": [
7 | {
8 | "begin": "2017-10-03 00:00:02",
9 | "end": "2017-10-03 00:00:12"
10 | },
11 | {
12 | "begin": "2017-10-03 00:00:32",
13 | "end": "2017-10-03 00:00:42"
14 | },
15 | {
16 | "begin": "2017-10-03 00:00:45",
17 | "end": "2017-10-03 00:00:52"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/demo/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dog-days/html5-player/94b6e34c39c3ec13f1d3f0166038d1d5618b5ed2/demo/public/logo.png
--------------------------------------------------------------------------------
/demo/public/thumbnail.vtt:
--------------------------------------------------------------------------------
1 | WEBVTT
2 |
3 | 00:00.000 --> 00:02.000
4 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=0,0,120,67
5 |
6 | 00:02.000 --> 00:04.000
7 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=120,0,120,67
8 |
9 | 00:04.000 --> 00:06.000
10 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=240,0,120,67
11 |
12 | 00:06.000 --> 00:08.000
13 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=360,0,120,67
14 |
15 | 00:08.000 --> 00:10.000
16 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=0,67,120,67
17 |
18 | 00:10.000 --> 00:12.000
19 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=120,67,120,67
20 |
21 | 00:12.000 --> 00:14.000
22 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=240,67,120,67
23 |
24 | 00:14.000 --> 00:16.000
25 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=360,67,120,67
26 |
27 | 00:16.000 --> 00:18.000
28 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=0,134,120,67
29 |
30 | 00:18.000 --> 00:20.000
31 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=120,134,120,67
32 |
33 | 00:20.000 --> 00:22.000
34 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=240,134,120,67
35 |
36 | 00:22.000 --> 00:24.000
37 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=360,134,120,67
38 |
39 | 00:24.000 --> 00:26.000
40 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=0,201,120,67
41 |
42 | 00:26.000 --> 00:28.000
43 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=120,201,120,67
44 |
45 | 00:28.000 --> 00:30.000
46 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=240,201,120,67
47 |
48 | 00:30.000 --> 00:33.041
49 | https://dog-days.github.io/demo/static/1g8jjku3-120.jpg#xywh=360,201,120,67
50 |
--------------------------------------------------------------------------------
/demo/public/umd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/demo/src/container.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Controller, { MemoryRouterController } from 'react-router-controller';
3 |
4 | Controller.set({
5 | readViewFile(viewId, controllerId, firstLoad) {
6 | //view可以异步载入
7 | return import(/* webpackMode: "eager" */
8 | `./view/${controllerId}/${viewId}/index.jsx`).then(component => {
9 | return component.default;
10 | });
11 | },
12 | readControllerFile(controllerId) {
13 | //webpackMode: eager是使import变为不异步,跟require一样,
14 | //但是返回的时promise对象,不能使用require,require会把没必要的文件载入
15 | //最好不使用异步载入,可能导致一些问题
16 | return import(/* webpackMode: "eager" */
17 | `./controller/${controllerId}.js`)
18 | .then(controller => {
19 | return controller.default;
20 | })
21 | .catch(e => {
22 | //必须catch并返回false
23 | return false;
24 | });
25 | },
26 | //设置首页path(跳转路径,即react-router path='/'时,会跳转到indexPath)
27 | //第一个字符必须是'/',不能是main/index,要是绝对的路径
28 | indexPath: () => {
29 | return '/main/basic';
30 | },
31 | });
32 |
33 | export default function container(props) {
34 | //basename的设置需要配合webpack使用,要不即使在开发环境没问题,但是成产环境
35 | //访问根目录的basename文件夹(文件名为basename的值),会有问题的。
36 | return (
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/demo/src/controller/main.js:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import Controller from 'react-router-controller';
3 |
4 | //内部依赖包
5 | import LayoutComponent from 'src/view/layout/main';
6 | const title = 'Html5 Player';
7 |
8 | export default class MainController extends Controller {
9 | LayoutComponent = LayoutComponent;
10 | indexView(params) {
11 | return this.render(
12 | {
13 | title,
14 | },
15 | params
16 | );
17 | }
18 | basicView(params) {
19 | return this.render(
20 | {
21 | title,
22 | },
23 | params
24 | );
25 | }
26 | //hls使用
27 | hlsView(params) {
28 | return this.render(
29 | {
30 | title,
31 | },
32 | params
33 | );
34 | }
35 | //flv使用
36 | flvView(params) {
37 | return this.render(
38 | {
39 | title,
40 | },
41 | params
42 | );
43 | }
44 | //tracks之字幕使用
45 | subtitleView(params) {
46 | return this.render(
47 | {
48 | title,
49 | },
50 | params
51 | );
52 | }
53 | //tracks之历史录像断片使用
54 | fragmentView(params) {
55 | return this.render(
56 | {
57 | title,
58 | },
59 | params
60 | );
61 | }
62 | customView(params) {
63 | return this.render(
64 | {
65 | title,
66 | },
67 | params
68 | );
69 | }
70 | thumbnailView(params) {
71 | return this.render(
72 | {
73 | title,
74 | },
75 | params
76 | );
77 | }
78 | mapView(params) {
79 | return this.render(
80 | {
81 | title,
82 | },
83 | params
84 | );
85 | }
86 | playlistView(params) {
87 | return this.render(
88 | {
89 | title,
90 | },
91 | params
92 | );
93 | }
94 | historyView(params) {
95 | return this.render(
96 | {
97 | title,
98 | },
99 | params
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/demo/src/index.jsx:
--------------------------------------------------------------------------------
1 | //begin---处理给非react使用者使用处理
2 | import nanPlayer from '../../src';
3 | import '../../src/style';
4 | window.nanPlayer = nanPlayer;
5 | //end----处理给非react使用者使用处理
6 |
7 | //下面的代码只针对React做例子
8 | import React from 'react';
9 | import { render } from 'react-dom';
10 | import Container from './container';
11 |
12 | function randomKey() {
13 | return Math.random()
14 | .toString(36)
15 | .substring(7)
16 | .split('')
17 | .join('.');
18 | }
19 |
20 | function renderApp(hot) {
21 | const dom = document.getElementById('app');
22 | if (dom) {
23 | render(, dom);
24 | }
25 | }
26 | renderApp();
27 | if (module.hot) {
28 | module.hot.accept('./container', () => {
29 | var hot = randomKey();
30 | return renderApp(hot);
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/demo/src/style/img/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/demo/src/view/layout/main/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import classnames from 'classnames';
4 |
5 | import 'src/style/css/bootstrap.css';
6 | import 'src/style/css/layout-main.less';
7 |
8 | class MainLayout extends React.Component {
9 | renderItem(title, viewId) {
10 | const { params } = this.props;
11 | return (
12 |
17 | {title}
18 |
19 | );
20 | }
21 | render() {
22 | const { children, params } = this.props;
23 | const codeUrl = `https://github.com/dog-days/html5-player/tree/master/demo/src/view/${
24 | params.controllerId
25 | }/${params.viewId}/index.jsx`;
26 | return (
27 |
28 |
44 |
45 |
51 | {children}
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | export default MainLayout;
59 |
--------------------------------------------------------------------------------
/demo/src/view/main/basic/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 | // import { joinUrlParams } from '../../../../../src/utils/util';
4 | function LoadingMessage(props) {
5 | console.log(props);
6 | return 超时第{props.count}次重连中...;
7 | }
8 | class View extends React.Component {
9 | state = {
10 | value:
11 | 'https://wowzaec2demo.streamlock.net/vod-multitrack/_definst_/smil:ElephantsDream/elephantsdream2.smil/playlist.m3u8',
12 | changeFile:
13 | 'https://wowzaec2demo.streamlock.net/vod-multitrack/_definst_/smil:ElephantsDream/elephantsdream2.smil/playlist.m3u8',
14 | };
15 | onInputChange = e => {
16 | this.setState({
17 | value: e.target.value,
18 | });
19 | };
20 | onKeyDown = e => {
21 | if (e.keyCode === 13) {
22 | const { value } = this.state;
23 | this.setState({
24 | changeFile: value,
25 | });
26 | }
27 | };
28 | videoCallback = player => {
29 | // player.on('error', () => {
30 | // clearTimeout(this.clearTimeout);
31 | // this.timeout = setTimeout(() => {
32 | // this.setState({
33 | // changeFile: joinUrlParams(this.state.changeFile, {}),
34 | // });
35 | // }, 3000);
36 | // });
37 | };
38 | render() {
39 | const { changeFile, value } = this.state;
40 | const file = changeFile;
41 | return (
42 |
43 |
44 |
50 |
}
52 | flvConfig={{ enableWorker: true }}
53 | livingMaxBuffer={3}
54 | videoCallback={this.videoCallback}
55 | timeout={1000 * 10}
56 | // contextMenu={[
demo,
demo2]}
57 | // playbackRates={[0.5, 1]}
58 | autoplay={true}
59 | // muted={true}
60 | retryTimes={5}
61 | controls={{
62 | capture: true,
63 | setting: true,
64 | rotate: true,
65 | dowload: (
66 |
72 |
75 |
76 | ),
77 | speed: true,
78 | }}
79 | title="这里是标题"
80 | file={file}
81 | // isLiving={true}
82 | // autoplay
83 | //logo支持string,React Element和plainObject
84 | // logo={`${process.env.basename}/logo.png`}
85 | // poster={`${process.env.basename}/logo.png`}
86 | />
87 |
88 |
89 | );
90 | }
91 | }
92 |
93 | export default View;
94 |
--------------------------------------------------------------------------------
/demo/src/view/main/custom/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Html5Player from 'html5-player';
4 |
5 | import Custom from './custom';
6 |
7 | export default class View extends React.Component {
8 | static contextTypes = {
9 | player: PropTypes.object,
10 | };
11 | state = {};
12 | render() {
13 | return (
14 |
15 |
16 |
17 | 自定义摄像机方向控制(通过播放器上面盖一层做处理,播放后,移上去就知道了。)
18 |
19 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/view/main/flv/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 |
4 | export default class View extends React.Component {
5 | render() {
6 | //线上直播找不到没跨域问题的
7 | return (
8 |
9 |
必须是.flv后缀名的文件,或者包含.flv的链接才可以播放。
10 |
11 |
12 |
13 |
14 |
15 | 如果使用直播,需要设置enableWorker,可以减少延时到1秒左右。
16 | 但是如果不是直播,不可以设置,否则会报错。
17 |
18 |
19 | 由于flv直播状态兼容性问题,需要通过设置isLiving=true来强制设置为直播状态。
20 |
21 |
22 | {``}
23 |
24 |
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/src/view/main/fragment/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 |
4 | export default class View extends React.Component {
5 | //录像断片处理
6 | render() {
7 | return (
8 |
9 |
10 | {
12 | // player.setSelection({
13 | // begin: 5,
14 | // end: 15,
15 | // minGap: 5,
16 | // maxGap: 30,
17 | // });
18 | // player.on('selection', data => {
19 | // console.log(player.currentTime);
20 | // console.log(data);
21 | // });
22 | }}
23 | // selection={true}
24 | file="https://media.w3.org/2010/05/sintel/trailer.mp4"
25 | fragment={{
26 | total: {
27 | begin: '2017-10-03 00:00:00',
28 | end: '2017-10-03 00:01:19',
29 | },
30 | fragments: [
31 | {
32 | begin: '2017-10-03 00:00:02',
33 | end: '2017-10-03 00:00:12',
34 | },
35 | {
36 | begin: '2017-10-03 00:00:32',
37 | end: '2017-10-03 00:00:42',
38 | },
39 | {
40 | begin: '2017-10-03 00:00:45',
41 | end: '2017-10-03 00:00:52',
42 | },
43 | ],
44 | }}
45 | />
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/demo/src/view/main/history/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Player from 'html5-player/libs/history';
3 |
4 | export default class View extends React.Component {
5 | state = {};
6 | //录像断片处理
7 | render() {
8 | return (
9 |
10 |
11 |
30 |
{
32 | this.player = player;
33 | player.on('selection', data => {
34 | console.log(player.currentTime);
35 | console.log(data);
36 | });
37 | player.on('fragmentInfo', data => {
38 | console.log(data);
39 | });
40 | }}
41 | selection={this.state.selection}
42 | defaultCurrentTime={60}
43 | historyList={{
44 | beginDate: '2018-07-28 00:00:00',
45 | duration: 20 + 654 + 12 + 52 + 52 + 10 + 654 + 20,
46 | fragments: [
47 | {
48 | begin: 0,
49 | end: 20,
50 | },
51 | {
52 | begin: 20,
53 | end: 20 + 654,
54 | file:
55 | 'https://wowzaec2demo.streamlock.net/vod-multitrack/_definst_/smil:ElephantsDream/elephantsdream2.smil/playlist.m3u8?test=2',
56 | },
57 | {
58 | begin: 20 + 654,
59 | end: 20 + 654 + 12,
60 | },
61 | {
62 | begin: 20 + 654 + 12,
63 | end: 20 + 654 + 12 + 52,
64 | file:
65 | 'https://media.w3.org/2010/05/sintel/trailer.mp4?test=2',
66 | },
67 | {
68 | begin: 20 + 654 + 12 + 52,
69 | end: 20 + 654 + 12 + 52 + 52,
70 | file:
71 | 'https://media.w3.org/2010/05/sintel/trailer.mp4?test=3',
72 | },
73 | {
74 | begin: 20 + 654 + 12 + 52 + 52,
75 | end: 20 + 654 + 12 + 52 + 52 + 10,
76 | },
77 | {
78 | begin: 20 + 654 + 12 + 52 + 52 + 10,
79 | end: 20 + 654 + 12 + 52 + 52 + 10 + 654,
80 | file:
81 | 'https://wowzaec2demo.streamlock.net/vod-multitrack/_definst_/smil:ElephantsDream/elephantsdream2.smil/playlist.m3u8',
82 | },
83 | {
84 | begin: 20 + 654 + 12 + 52 + 52 + 10 + 654,
85 | end: 20 + 654 + 12 + 52 + 52 + 10 + 654 + 20,
86 | },
87 | ],
88 | }}
89 | />
90 |
91 |
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/demo/src/view/main/hls/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 |
4 | export default class View extends React.Component {
5 | //必须是.m3u8或者.m3u后缀名的文件才可以播放。
6 | render() {
7 | return (
8 |
9 |
必须是.m3u8或者.m3u后缀名的文件才可以播放。
10 |
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/src/view/main/index/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class IndexView extends React.Component {
4 | render() {
5 | console.debug('index页面');
6 | return 主页页面
;
7 | }
8 | }
9 |
10 | export default IndexView;
11 |
--------------------------------------------------------------------------------
/demo/src/view/main/map/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Html5Player from 'html5-player';
4 |
5 | import addEventListener from '../../../../../src/utils/dom/addEventListener';
6 |
7 | const AMap = window.AMap;
8 | const AMapUI = window.AMapUI;
9 |
10 | export default class View extends React.Component {
11 | componentDidMount() {
12 | const _this = this;
13 | //创建地图
14 | var map = new AMap.Map('demo-container', {
15 | zoom: 4,
16 | });
17 |
18 | AMapUI.loadUI(['overlay/SimpleInfoWindow'], SimpleInfoWindow => {
19 | var marker = new AMap.Marker({
20 | map: map,
21 | zIndex: 9999999,
22 | position: map.getCenter(),
23 | });
24 |
25 | var infoWindow = new SimpleInfoWindow({
26 | infoTitle: '这里是标题',
27 | infoBody: '',
28 |
29 | //基点指向marker的头部位置
30 | offset: new AMap.Pixel(0, -31),
31 | });
32 | let closeEvent;
33 | function openInfoWin() {
34 | infoWindow.open(map, marker.getPosition());
35 | if (closeEvent && closeEvent.remove) {
36 | closeEvent.remove();
37 | }
38 | closeEvent = addEventListener(
39 | infoWindow
40 | .get$Container()[0]
41 | .querySelector('.amap-ui-infowindow-close'),
42 | 'click',
43 | () => {
44 | ReactDOM.render(
45 | ,
46 | document.getElementById('amap-container')
47 | );
48 | closeEvent.remove();
49 | }
50 | );
51 | }
52 |
53 | //marker 点击时打开
54 | AMap.event.addListener(marker, 'click', () => {
55 | openInfoWin();
56 | ReactDOM.render(
57 | this.renderPlayer(),
58 | document.getElementById('amap-container')
59 | );
60 | });
61 | openInfoWin();
62 | setTimeout(function() {
63 | ReactDOM.render(
64 | _this.renderPlayer(),
65 | document.getElementById('amap-container')
66 | );
67 | }, 200);
68 | });
69 | }
70 | renderPlayer() {
71 | return (
72 |
87 | );
88 | }
89 | render() {
90 | return (
91 |
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/demo/src/view/main/playlist/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5PlayerList from 'html5-player/libs/playlist';
3 |
4 | export default class View extends React.Component {
5 | render() {
6 | const playlist = [];
7 | for (var i = 0; i < 20; i++) {
8 | let obj = {
9 | title: `第${i + 1}集`,
10 | cover:
11 | 'https://t12.baidu.com/it/u=2991737441,599903151&fm=173&app=25&f=JPEG?w=538&h=397&s=ECAA21D53C330888369488B703006041',
12 | };
13 | //random是为了让file不一样,一样的file切换的时候是不会重新载入的。
14 | if (i % 2 === 0) {
15 | obj.file = `https://wowzaec2demo.streamlock.net/live/bigbuckbunny/playlist.m3u8?random=${Math.random()}`;
16 | } else {
17 | obj.file = `https://dog-days.github.io/demo/static/react.mp4?random=${Math.random()}`;
18 | }
19 | playlist.push(obj);
20 | }
21 | return (
22 |
23 |
播放列表
24 |
25 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/demo/src/view/main/subtitle/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 |
4 | export default class View extends React.Component {
5 | //必须是.m3u8或者.m3u后缀名的文件才可以播放。
6 | render() {
7 | return (
8 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/view/main/thumbnail/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Html5Player from 'html5-player';
3 |
4 | export default class View extends React.Component {
5 | //必须是.m3u8或者.m3u后缀名的文件才可以播放。
6 | render() {
7 | return (
8 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/scripts/build-demo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | console.log();
3 | console.log('Building...');
4 | console.log('This might take a couple minutes.');
5 | console.log();
6 |
7 | process.env.NODE_ENV = 'production';
8 |
9 | const util = require('react-boilerplate-app-utils');
10 | const scriptsPackagename = util.scriptsPackagename;
11 | const path = require('path');
12 | const fs = require('fs-extra');
13 | const chalk = require('chalk');
14 | const paths = require(util.pathResolve('config/paths.js', scriptsPackagename));
15 | const webpack = require('webpack');
16 | const config = require('../config/webpack.config.demo');
17 | const Table = require('cli-table');
18 | const gzipSize = require('gzip-size').sync;
19 |
20 | const topBuildFolder = paths.appBuild;
21 | //清空build文件夹
22 | fs.emptyDirSync(topBuildFolder);
23 |
24 | webpack(config).run(function(err, stats) {
25 | if (err) {
26 | console.log(chalk.red('Failed to build.'));
27 | console.error(err.stack || err);
28 | if (err.details) {
29 | console.error(err.details);
30 | }
31 | return;
32 | }
33 |
34 | //复制除了index.html外的静态文件,所以public文件夹不要放不使用的文件
35 | fs.copySync(paths.appPublic, paths.appBuild, {
36 | dereference: true,
37 | filter: file => {
38 | if (file === paths.appHtml) {
39 | return false;
40 | }
41 | //public/mock目录不复制
42 | if (file === path.resolve(paths.appPublic, 'mock')) {
43 | return false;
44 | }
45 | //public/websocket目录不复制
46 | if (file === path.resolve(paths.appPublic, 'websocket')) {
47 | return false;
48 | }
49 | return true;
50 | },
51 | });
52 |
53 | const info = stats.toJson();
54 | if (stats.hasErrors()) {
55 | util.printValidationResults(info.errors, 'error');
56 | }
57 |
58 | if (stats.hasWarnings()) {
59 | util.printValidationResults(info.warnings, 'warning');
60 | }
61 | if (info.assets && info.assets[0]) {
62 | //处理header
63 | var head = ['Asset', 'Real Size', 'Gzip Size', 'Chunks', '', 'Chunk Names'];
64 | head = head.reduce((a, b) => {
65 | a.push(chalk.cyan(b));
66 | return a;
67 | }, []);
68 | var table = new Table({
69 | head,
70 | });
71 | info.assets.forEach(v => {
72 | var sizeAfterGzip;
73 | if (v.name.match(/(.js$)|(.css$)/)) {
74 | var fileContents = fs.readFileSync(
75 | path.resolve(paths.appBuild, v.name)
76 | );
77 | console.log(paths.appBuild, v.name);
78 | sizeAfterGzip = gzipSize(fileContents);
79 | }
80 | table.push([
81 | chalk.green(v.name),
82 | util.transformToKBMBGB(v.size, { decimals: 2 }),
83 | sizeAfterGzip
84 | ? util.transformToKBMBGB(sizeAfterGzip, { decimals: 2 })
85 | : '',
86 | v.chunks,
87 | v.emitted ? chalk.green('[emitted]') : '',
88 | v.chunkNames,
89 | ]);
90 | });
91 | console.log(`Hash: ${chalk.cyan(info.hash)}`);
92 | console.log(`Version: ${chalk.cyan(info.version)}`);
93 | console.log(`Time: ${chalk.cyan(info.time / 1000 + 's')}`);
94 | console.log();
95 | console.log(table.toString());
96 | console.log();
97 | const useYarn = util.shouldUseYarn();
98 | console.log(`The ${chalk.cyan('build')} folder: `);
99 | console.log(chalk.cyan(topBuildFolder));
100 | console.log('is ready to be served.');
101 | console.log('You may serve it with a static server:');
102 | console.log();
103 | var displayedCommand = 'npm run';
104 | if (useYarn) {
105 | displayedCommand = 'yarn';
106 | }
107 | console.log(chalk.cyan(` ${displayedCommand} serve-demo-build`));
108 | }
109 | });
110 |
--------------------------------------------------------------------------------
/scripts/build-umd.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | console.log();
3 | console.log('Building...');
4 | console.log('This might take a couple minutes.');
5 | console.log();
6 |
7 | process.env.NODE_ENV = 'production';
8 |
9 | const util = require('react-boilerplate-app-utils');
10 | const scriptsPackagename = util.scriptsPackagename;
11 | const path = require('path');
12 | const fs = require('fs-extra');
13 | const chalk = require('chalk');
14 | const paths = require(util.pathResolve('config/paths.js', scriptsPackagename));
15 | const webpack = require('webpack');
16 | const config = require('../config/webpack.config.umd');
17 | const Table = require('cli-table');
18 | const gzipSize = require('gzip-size').sync;
19 |
20 | const topBuildFolder = paths.umdBuild;
21 | //清空build文件夹
22 | fs.emptyDirSync(topBuildFolder);
23 |
24 | webpack(config).run(function(err, stats) {
25 | if (err) {
26 | console.log(chalk.red('Failed to build.'));
27 | console.error(err.stack || err);
28 | if (err.details) {
29 | console.error(err.details);
30 | }
31 | return;
32 | }
33 |
34 | const info = stats.toJson();
35 | if (stats.hasErrors()) {
36 | util.printValidationResults(info.errors, 'error');
37 | }
38 |
39 | if (stats.hasWarnings()) {
40 | util.printValidationResults(info.warnings, 'warning');
41 | }
42 | if (info.assets && info.assets[0]) {
43 | fs.copySync(
44 | path.resolve(paths.appPublic, 'umd.html'),
45 | path.resolve(topBuildFolder, 'index.html')
46 | );
47 | //处理header
48 | var head = ['Asset', 'Real Size', 'Gzip Size', 'Chunks', '', 'Chunk Names'];
49 | head = head.reduce((a, b) => {
50 | a.push(chalk.cyan(b));
51 | return a;
52 | }, []);
53 | var table = new Table({
54 | head,
55 | });
56 | info.assets.forEach(v => {
57 | var sizeAfterGzip;
58 | if (v.name.match(/(.js$)|(.css$)/)) {
59 | var fileContents = fs.readFileSync(
60 | path.resolve(topBuildFolder, v.name)
61 | );
62 | sizeAfterGzip = gzipSize(fileContents);
63 | }
64 | table.push([
65 | chalk.green(v.name),
66 | util.transformToKBMBGB(v.size, { decimals: 2 }),
67 | sizeAfterGzip
68 | ? util.transformToKBMBGB(sizeAfterGzip, { decimals: 2 })
69 | : '',
70 | v.chunks,
71 | v.emitted ? chalk.green('[emitted]') : '',
72 | v.chunkNames,
73 | ]);
74 | });
75 | console.log(`Hash: ${chalk.cyan(info.hash)}`);
76 | console.log(`Version: ${chalk.cyan(info.version)}`);
77 | console.log(`Time: ${chalk.cyan(info.time / 1000 + 's')}`);
78 | console.log();
79 | console.log(table.toString());
80 | console.log();
81 | const useYarn = util.shouldUseYarn();
82 | console.log(`The ${chalk.cyan('build')} folder: `);
83 | console.log(chalk.cyan(topBuildFolder));
84 | console.log('is ready to be served.');
85 | console.log('You may serve it with a static server:');
86 | console.log();
87 | var displayedCommand = 'npm run';
88 | if (useYarn) {
89 | displayedCommand = 'yarn';
90 | }
91 | console.log(chalk.cyan(` ${displayedCommand} serve-umd-build`));
92 | }
93 | });
94 |
--------------------------------------------------------------------------------
/scripts/read-and-wirte-version.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs-extra');
4 | const path = require('path');
5 | const version = require('../package.json').version;
6 | //向src/verison.js写入version信息。
7 | fs.writeFileSync(
8 | path.resolve(process.cwd(), 'src/version.js'),
9 | `window.html5PlayerVersion = '${version}';\n`
10 | );
11 |
--------------------------------------------------------------------------------
/src/api/flvjs-api.js:
--------------------------------------------------------------------------------
1 | import API from './api';
2 | import * as logger from '../utils/logger';
3 | import { DEBUG } from '../utils//const';
4 |
5 | export default class flvAPI extends API {
6 | constructor(videoDOM, file, flvjs, flvConfig = {}) {
7 | let _this = super(videoDOM, file);
8 | this.flvConfig = flvConfig;
9 | if (flvjs.isSupported()) {
10 | flvjs.LoggingControl.enableDebug = false;
11 | flvjs.LoggingControl.enableVerbose = false;
12 | flvjs.LoggingControl.enableWarn = false;
13 | this.flvjs = flvjs;
14 | }
15 | return _this;
16 | }
17 |
18 | detachMedia() {
19 | const player = this.flvPlayer;
20 | if (player) {
21 | this.pause();
22 | this.detachEvent();
23 | player.unload();
24 | player.detachMediaElement();
25 | player.destroy();
26 | this.flvPlayer = null;
27 | logger.success('Detach Media:', 'detach media for flv.js sucessfully.');
28 | }
29 | this.videoDOM.src = '';
30 | }
31 | //载入视频源,这里不可以用箭头函数
32 | loadSource(file) {
33 | const flvjs = this.flvjs;
34 | if (flvjs) {
35 | const flvPlayer = flvjs.createPlayer(
36 | {
37 | type: 'flv',
38 | url: file,
39 | },
40 | {
41 | ...this.flvConfig,
42 | }
43 | );
44 | this.flvPlayer = flvPlayer;
45 | flvPlayer.attachMediaElement(this);
46 | flvPlayer.load();
47 | logger.info('Source Loading :', 'loading flv video.');
48 | //flv的log事件是全局的,这是个坑
49 | this.attachEvent();
50 | }
51 | }
52 | detachEvent() {
53 | if (this.LoggingControlListener && DEBUG) {
54 | this.flvjs.LoggingControl.removeLogListener(this.LoggingControlListener);
55 | }
56 | }
57 | attachEvent() {
58 | if (!DEBUG) {
59 | return;
60 | }
61 | // const locale = this.localization;
62 | const errorTitle = 'Flv.js Error,';
63 | if (this.flvjs) {
64 | this.LoggingControlListener = (type, str) => {
65 | if (type === 'error') {
66 | // let message;
67 | if (~str.indexOf('IOController')) {
68 | logger.error(errorTitle, `load error`);
69 | // message = locale.fileCouldNotPlay;
70 | // //一般trigger都是为了对外提供api,error是个比较特殊的情况,寄对外提供了事件,也对内提供了事件。
71 | // //如果只是对内不对外的话,不可以使用trigger处理事件,所有的都用redux。
72 | // this.event.trigger('error', {
73 | // data: str,
74 | // message,
75 | // type,
76 | // });
77 | }
78 | }
79 | };
80 |
81 | this.flvjs.LoggingControl.addLogListener(this.LoggingControlListener);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/api/html5-api.js:
--------------------------------------------------------------------------------
1 | import API from './api';
2 | import * as logger from '../utils/logger';
3 |
4 | export default class html5API extends API {
5 | constructor(videoDOM, file) {
6 | let _this = super(videoDOM, file);
7 | return _this;
8 | }
9 | loadSource(file) {
10 | this.src = file;
11 | logger.info('Source Loading:', 'loading h5 video.');
12 | }
13 | detachMedia() {
14 | //必须设置src为空,浏览原生播放器才会断流,原生hls就有这种问题
15 | this.videoDOM.src = '';
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/css/_const.less:
--------------------------------------------------------------------------------
1 | @dark-color: #000;
2 | @text-color: rgba(255, 255, 255, 0.8);
3 | @text-light-color: #fff;
4 | @icon-color: rgba(255, 255, 255, 0.8);
5 | @icon-hover-color: rgba(255, 255, 255, 1);
6 | //slider 水平或垂直大小(高或宽度大小)
7 | @slider-size: 6px;
8 | //slider圆点大小
9 | @slider-circle-size: 14px;
10 | @slider-circle-color: #108ee9;
11 | //slider底部整条颜色
12 | @slider-rail-color: rgba(255, 255, 255, 0.4);
13 | @slider-history-rail-color: #7ec2f3;
14 | @slider-broken-color: #656565;
15 | //slider会长度会变化的元素颜色
16 | @slider-track-color: #108ee9;
17 | @slider-buffer-color: rgba(255, 255, 255, 0.6);
18 | @time-tooltip-content-bg: rgba(255, 255, 255, 1);
19 | //tooptip背景
20 | @tooltip-bg: #191919;
21 | //ul li
22 | @item-hover-color: #303030;
23 |
--------------------------------------------------------------------------------
/src/assets/css/_mixin.less:
--------------------------------------------------------------------------------
1 | @import '_const';
2 |
3 | .black-bg(@opacity: 1) {
4 | background-color: rgba(0, 0, 0, @opacity);
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/css/animate.less:
--------------------------------------------------------------------------------
1 | .html5-player-container {
2 | @keyframes spin {
3 | 100% {
4 | transform: rotate(360deg);
5 | }
6 | }
7 | @keyframes zoomIn {
8 | from {
9 | opacity: 0;
10 | transform: scale3d(0.3, 0.3, 0.3);
11 | }
12 |
13 | 50% {
14 | opacity: 1;
15 | }
16 | }
17 | @keyframes zoomOut {
18 | from {
19 | opacity: 1;
20 | }
21 |
22 | 50% {
23 | opacity: 0;
24 | transform: scale3d(0.3, 0.3, 0.3);
25 | }
26 |
27 | to {
28 | opacity: 0;
29 | }
30 | }
31 | @keyframes fadeIn {
32 | from {
33 | opacity: 0;
34 | }
35 |
36 | to {
37 | opacity: 1;
38 | }
39 | }
40 | @keyframes fadeOut {
41 | from {
42 | opacity: 1;
43 | }
44 |
45 | to {
46 | opacity: 0;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/assets/icon/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dog-days/html5-player/94b6e34c39c3ec13f1d3f0166038d1d5618b5ed2/src/assets/icon/iconfont.eot
--------------------------------------------------------------------------------
/src/assets/icon/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dog-days/html5-player/94b6e34c39c3ec13f1d3f0166038d1d5618b5ed2/src/assets/icon/iconfont.ttf
--------------------------------------------------------------------------------
/src/assets/icon/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dog-days/html5-player/94b6e34c39c3ec13f1d3f0166038d1d5618b5ed2/src/assets/icon/iconfont.woff
--------------------------------------------------------------------------------
/src/assets/img/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/history.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Provider from './provider';
4 | import HistorPlayer from './view/history';
5 |
6 | export default function(props) {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/i18n/default.js:
--------------------------------------------------------------------------------
1 | import locale from './zh_CN';
2 | export default locale;
3 |
--------------------------------------------------------------------------------
/src/i18n/zh_CN.js:
--------------------------------------------------------------------------------
1 | export default {
2 | loadingPlayerText: '播放器加载中...',
3 | unknownError: '视频加载出错',
4 | fileCouldNotPlay: '视频加载出错',
5 | timeout: '视频加载超时',
6 | speed: '倍速',
7 | normalSpeed: '正常',
8 | videoNotSupport: '视频加载出错',
9 | subtitle: '字幕',
10 | subtitleOff: '关闭',
11 | autoQuality: '自动',
12 | pictureQuality: '画质',
13 | reloading: '超时重连中...',
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //内部依赖包
4 | import Provider from './provider';
5 | import View from './view';
6 |
7 | export default function player(props) {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/libs/fetch/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // `baseURL` will be prepended to `url` unless `url` is absolute.
3 | // It can be convenient to set `baseURL` for an instance of axios to pass relative URLs
4 | // to methods of that instance.
5 | baseURL: '',
6 |
7 | // `timeout` specifies the number of milliseconds before the request times out.
8 | // If the request takes longer than `timeout`, the request will be aborted.
9 | timeout: 5000,
10 |
11 | // `withCredentials` indicates whether or not cross-site Access-Control requests
12 | // should be made using credentials
13 | withCredentials: false, // 允许跨域传递cookie
14 |
15 | // `responseType` indicates the type of data that the server will respond with
16 | // options are 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
17 | responseType: 'json', // default
18 |
19 | // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
20 | xsrfCookieName: 'XSRF-TOKEN', // default
21 |
22 | // `xsrfHeaderName` is the name of the http header that carries the xsrf token value
23 | xsrfHeaderName: 'X-XSRF-TOKEN', // default
24 |
25 | // `maxContentLength` defines the max size of the http response content allowed
26 | maxContentLength: 2000,
27 |
28 | // `validateStatus` defines whether to resolve or reject the promise for a given
29 | // HTTP response status code. If `validateStatus` returns `true` (or is set to `null`
30 | // or `undefined`), the promise will be resolved; otherwise, the promise will be
31 | // rejected.
32 | validateStatus: function(status) {
33 | return status >= 200 && status < 300; // default
34 | },
35 |
36 | // `maxRedirects` defines the maximum number of redirects to follow in node.js.
37 | // If set to 0, no redirects will be followed.
38 | maxRedirects: 5, // default
39 | };
40 |
--------------------------------------------------------------------------------
/src/libs/provider/redux-provider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import window from 'global/window';
6 |
7 | /**
8 | * redux 基础 provider
9 | * @prop { object } store redux store
10 | * 非必要的,如果有store,reducers、middlewares和enhancers,preloadedState就不生效,传进来也没有意义
11 | * @prop { array } middlewares redux middlewares
12 | * @prop { array } enhancers redux enhancers
13 | * @prop { object } reducers redux reducers (传进来后会被combineReducers)
14 | * @prop { any } preloadedState redux preloadedState
15 | * @prop { boolean } production 是否是生产环境
16 | */
17 | export default class ReduxProvider extends React.Component {
18 | static propTypes = {
19 | //非必要的,如果有store,reducers、middlewares和enhancers就不生效,传进来也没有意义
20 | store: PropTypes.object,
21 | middlewares: PropTypes.array,
22 | enhancers: PropTypes.array,
23 | preloadedState: PropTypes.any,
24 | reducers: PropTypes.object,
25 | production: PropTypes.bool,
26 | };
27 | displayName = 'Provider';
28 | state = {};
29 | getStore() {
30 | let { store, preloadedState } = this.props;
31 | if (!store) {
32 | const reducers = this.getReducers(this.props);
33 | const enhancers = this.getEnhancers(this.props);
34 | store = createStore(reducers, preloadedState, enhancers);
35 | }
36 | return store;
37 | }
38 | getReducers(props) {
39 | const { reducers } = props;
40 | return combineReducers({
41 | ...reducers,
42 | });
43 | }
44 | getEnhancers(props) {
45 | const { enhancers = [], middlewares = [], production = true } = props;
46 | let devtools = () => noop => noop;
47 | //如果localStorage.reduxTools=true,reduxTools强制打开。
48 | //给生产环境调试使用。
49 | if (
50 | (!production || JSON.parse(localStorage.getItem('reduxDevTools'))) &&
51 | window.__REDUX_DEVTOOLS_EXTENSION__
52 | ) {
53 | devtools = window.__REDUX_DEVTOOLS_EXTENSION__;
54 | } else {
55 | console.log('You have not install the redux-devtools-extension.');
56 | console.log(
57 | 'See https://github.com/zalmoxisus/redux-devtools-extension.'
58 | );
59 | }
60 | const _middlewares = [...middlewares];
61 | const _enhancers = [
62 | applyMiddleware(..._middlewares),
63 | devtools(),
64 | ...enhancers,
65 | ];
66 | return compose(..._enhancers);
67 | }
68 | componentWillReceiveProps(nextProps) {
69 | const { store, hot } = nextProps;
70 | //热替换处理,根据props.hot来处理
71 | if (!store && hot) {
72 | const reducers = this.getReducers(nextProps);
73 | this.store.replaceReducer(reducers);
74 | }
75 | }
76 | componentWillUnmount() {
77 | this.store = null;
78 | }
79 | render() {
80 | const { children } = this.props;
81 | if (!this.store) {
82 | this.store = this.getStore();
83 | }
84 | return {children};
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/libs/provider/saga-model-provider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Provider from './redux-provider';
4 | import getSagaModel from './saga-model';
5 |
6 | /**
7 | * @prop { array } middlewares redux middlewares
8 | * @prop { array } enhancers redux enhancers
9 | * @prop { object } reducers redux reducers (传进来后会被combineReducers)
10 | * @prop { any } preloadedState redux preloadedState
11 | * @prop { boolean } production 是否是生产环境,默认为true
12 | */
13 | export default class ModelProvider extends React.Component {
14 | static propTypes = {
15 | middlewares: PropTypes.array,
16 | preloadedState: PropTypes.any,
17 | enhancers: PropTypes.array,
18 | reducers: PropTypes.object,
19 | production: PropTypes.bool,
20 | };
21 | static childContextTypes = {
22 | sagaStore: PropTypes.object,
23 | prefix: PropTypes.string,
24 | };
25 | getChildContext() {
26 | return {
27 | sagaStore: this.store,
28 | prefix: this.props.prefix,
29 | };
30 | }
31 | displayName = 'SagaModelProvider';
32 | state = {};
33 | componentWillUnmount() {
34 | this.store = null;
35 | }
36 | getStore() {
37 | const {
38 | reducers,
39 | preloadedState,
40 | models = [],
41 | middlewares = [],
42 | plugins = [],
43 | production = true,
44 | } = this.props;
45 | const sagaModel = getSagaModel(
46 | reducers,
47 | preloadedState,
48 | models,
49 | middlewares,
50 | plugins,
51 | !production
52 | );
53 | const store = sagaModel.store();
54 | return store;
55 | }
56 |
57 | render() {
58 | const { children, production } = this.props;
59 | if (!this.store) {
60 | this.store = this.getStore();
61 | }
62 | return (
63 |
64 | {children}
65 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/libs/provider/saga-model.js:
--------------------------------------------------------------------------------
1 | import { SagaModel } from 'redux-saga-model';
2 |
3 | /**
4 | * 获取saga model实例化对象
5 | * @param { object } history 同react-router history
6 | * @param { object } reducers 为经过combineReducer的reducers
7 | * @param { any } preloadedState 请参考redux的createStore第二参数
8 | * @param { array } models 请参考redux-saga-model说明
9 | * @param { array } plugins 请参考redux-saga-model说明
10 | */
11 | export default function getSagaModel(
12 | reducers,
13 | preloadedState,
14 | models = [],
15 | middlewares = [],
16 | plugins = [],
17 | openReduxDevTools
18 | ) {
19 | const initialState = preloadedState;
20 | const initialReducer = {
21 | ...reducers,
22 | };
23 | const initialMiddleware = [...middlewares];
24 | const initialModels = models;
25 | const sagaModel = new SagaModel({
26 | initialState,
27 | initialReducer,
28 | initialMiddleware,
29 | initialModels,
30 | history,
31 | openReduxDevTools,
32 | });
33 | plugins.forEach(sagaModel.use.bind(sagaModel));
34 | return sagaModel;
35 | }
36 |
--------------------------------------------------------------------------------
/src/libs/query-string/decode-uri-component.js:
--------------------------------------------------------------------------------
1 | const token = '%[a-f0-9]{2}';
2 | const singleMatcher = new RegExp(token, 'gi');
3 | const multiMatcher = new RegExp('(' + token + ')+', 'gi');
4 |
5 | function decodeComponents(components, split) {
6 | try {
7 | // Try to decode the entire string first
8 | return decodeURIComponent(components.join(''));
9 | } catch (err) {
10 | // Do nothing
11 | }
12 |
13 | if (components.length === 1) {
14 | return components;
15 | }
16 |
17 | split = split || 1;
18 |
19 | // Split the array in 2 parts
20 | var left = components.slice(0, split);
21 | var right = components.slice(split);
22 |
23 | return Array.prototype.concat.call(
24 | [],
25 | decodeComponents(left),
26 | decodeComponents(right)
27 | );
28 | }
29 |
30 | function decode(input) {
31 | try {
32 | return decodeURIComponent(input);
33 | } catch (err) {
34 | var tokens = input.match(singleMatcher);
35 |
36 | for (var i = 1; i < tokens.length; i++) {
37 | input = decodeComponents(tokens, i).join('');
38 |
39 | tokens = input.match(singleMatcher);
40 | }
41 |
42 | return input;
43 | }
44 | }
45 |
46 | function customDecodeURIComponent(input) {
47 | // Keep track of all the replacements and prefill the map with the `BOM`
48 | var replaceMap = {
49 | '%FE%FF': '\uFFFD\uFFFD',
50 | '%FF%FE': '\uFFFD\uFFFD',
51 | };
52 |
53 | var match = multiMatcher.exec(input);
54 | while (match) {
55 | try {
56 | // Decode as big chunks as possible
57 | replaceMap[match[0]] = decodeURIComponent(match[0]);
58 | } catch (err) {
59 | var result = decode(match[0]);
60 |
61 | if (result !== match[0]) {
62 | replaceMap[match[0]] = result;
63 | }
64 | }
65 |
66 | match = multiMatcher.exec(input);
67 | }
68 |
69 | // Add `%C2` at the end of the map to make sure it does not replace the combinator before everything else
70 | replaceMap['%C2'] = '\uFFFD';
71 |
72 | var entries = Object.keys(replaceMap);
73 |
74 | for (var i = 0; i < entries.length; i++) {
75 | // Replace all decoded components
76 | var key = entries[i];
77 | input = input.replace(new RegExp(key, 'g'), replaceMap[key]);
78 | }
79 |
80 | return input;
81 | }
82 |
83 | export default function(encodedURI) {
84 | if (typeof encodedURI !== 'string') {
85 | throw new TypeError(
86 | 'Expected `encodedURI` to be of type `string`, got `' +
87 | typeof encodedURI +
88 | '`'
89 | );
90 | }
91 |
92 | try {
93 | encodedURI = encodedURI.replace(/\+/g, ' ');
94 |
95 | // Try the built in decoder first
96 | return decodeURIComponent(encodedURI);
97 | } catch (err) {
98 | // Fallback to a more advanced decoder
99 | return customDecodeURIComponent(encodedURI);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/loader.js:
--------------------------------------------------------------------------------
1 | import * as logger from './utils/logger';
2 |
3 | export function chunkLoadErrorHandler(error, type = 'h5') {
4 | // Webpack require.ensure error: "Loading chunk 3 failed"
5 | logger.error('Module Loaded:', 'relative module loaded failed.');
6 | throw error;
7 | }
8 | //通过配置判断载入的文件
9 | export default function selectBundle(config = {}) {
10 | let bundle;
11 | if (config.hlsjs) {
12 | bundle = loadHlsJsBundle(config);
13 | } else if (config.flvjs) {
14 | bundle = loadFlvJsBundle(config);
15 | } else {
16 | bundle = loadHtml5Bundle(config);
17 | }
18 | return bundle;
19 | }
20 |
21 | function loadFlvJsBundle(config) {
22 | return new Promise(function(resolve) {
23 | require.ensure(
24 | ['flv.ly.js/dist/flv.js', './api/flvjs-api'],
25 | function(require) {
26 | const flvjs = require('flv.ly.js/dist/flv.js');
27 | logger.success(
28 | 'Module Loaded:',
29 | 'relative flv.js module loaded sucessfully.'
30 | );
31 | const apiClass = require('./api/flvjs-api').default;
32 | apiClass.prototype.localization = config.localization;
33 | if (config.flvConfig && config.flvConfig.enableWorker) {
34 | //worker,多线程多线程转换流,减少延时(2秒左右)
35 | config.flvConfig = {
36 | ...config.flvConfig,
37 | ...{
38 | enableWorker: true,
39 | // lazyLoad: false,
40 | //Indicates how many seconds of data to be kept for lazyLoad.
41 | // lazyLoadMaxDuration: 0,
42 | // autoCleanupMaxBackwardDuration: 3,
43 | // autoCleanupMinBackwardDuration: 2,
44 | // autoCleanupSourceBuffer: true,
45 | enableStashBuffer: false,
46 | stashInitialSize: 128,
47 | isLive: true,
48 | },
49 | };
50 | }
51 | const api = new apiClass(
52 | config.videoDOM,
53 | config.file,
54 | flvjs,
55 | config.flvConfig
56 | );
57 | resolve({
58 | flvjs,
59 | api,
60 | });
61 | },
62 | chunkLoadErrorHandler,
63 | 'provider.flvjs'
64 | );
65 | });
66 | }
67 |
68 | function loadHlsJsBundle(config) {
69 | return new Promise(function(resolve) {
70 | require.ensure(
71 | ['hls.js', './api/hlsjs-api', './model/video/events/hlsjs'],
72 | function(require) {
73 | const hlsjs = require('hls.js');
74 | logger.success(
75 | 'Module Loaded:',
76 | 'relative hls.js module loaded sucessfully.'
77 | );
78 | const apiClass = require('./api/hlsjs-api').default;
79 | apiClass.prototype.localization = config.localization;
80 | const api = new apiClass(config.videoDOM, config.file, hlsjs);
81 | const hlsjsEvents = require('./model/video/events/hlsjs').default;
82 | resolve({
83 | hlsjs,
84 | api,
85 | hlsjsEvents,
86 | });
87 | },
88 | chunkLoadErrorHandler,
89 | 'provider.hlsjs'
90 | );
91 | });
92 | }
93 |
94 | function loadHtml5Bundle(config) {
95 | return new Promise(function(resolve) {
96 | const apiClass = require('./api/html5-api').default;
97 | apiClass.prototype.localization = config.localization;
98 | const api = new apiClass(config.videoDOM, config.file);
99 | resolve({
100 | api,
101 | });
102 | });
103 | }
104 |
--------------------------------------------------------------------------------
/src/model-list.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'video/index',
3 | 'ready',
4 | 'loading',
5 | 'end',
6 | 'play-pause',
7 | 'volume',
8 | 'muted',
9 | 'living',
10 | 'time',
11 | 'fullscreen',
12 | 'not-autoplay',
13 | 'controlbar',
14 | 'time-slider',
15 | 'error-message',
16 | 'track',
17 | 'fragment',
18 | 'playback-rate',
19 | 'picture-quality',
20 | 'rotate',
21 | 'selection',
22 | 'history',
23 | ];
24 |
--------------------------------------------------------------------------------
/src/model/controlbar.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'controlbar';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | controlbarReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *controlbarSaga({ payload }, { put }) {
16 | yield put({
17 | type: `controlbarReducer`,
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/end.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'end';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | endStateReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *endStateSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'endStateReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/error-message.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'error-message';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: {
6 | message: null,
7 | },
8 | reducers: {
9 | errorMessageReducer: function(state, { payload }) {
10 | return payload;
11 | },
12 | clear: function(state, { payload }) {
13 | return this.state;
14 | },
15 | },
16 | sagas: {
17 | *errorMessageSaga({ payload }, { put }) {
18 | yield put({
19 | type: 'errorMessageReducer',
20 | payload,
21 | });
22 | },
23 | },
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/model/fragment.js:
--------------------------------------------------------------------------------
1 | import { standardReducer } from '../utils/util';
2 | import fetch from '../utils/fetch';
3 | import { hms } from '../utils/util';
4 | import isString from 'lodash/isString';
5 |
6 | //请求取消函数
7 | let cancel;
8 |
9 | function fetchFragment(url, params) {
10 | const cancelSource = fetch.getCancelSource();
11 | cancel = cancelSource.cancel;
12 | return fetch
13 | .get(url, { params, cancelToken: cancelSource.token })
14 | .catch(error => {
15 | return false;
16 | });
17 | }
18 |
19 | export const namespace = 'fragment';
20 | export default function() {
21 | return {
22 | namespace,
23 | state: {
24 | percent: null,
25 | duration: null,
26 | timeDuration: null,
27 | data: null,
28 | },
29 | reducers: {
30 | fragmentReducer: standardReducer,
31 | //合成录像,摄像头上传视频会中断,会分成几个视频,然后这几个视频会合并成一个视频
32 | //但是这个视频不是整个时段的,会有断的,为了给用户知道这段录像哪里断了,需要而外处理
33 | //sldierReducer这里是为了算出播放中,遇到断片的情况,进行一些跳过处理
34 | sliderReducer: standardReducer,
35 | clear: function(state, { payload }) {
36 | cancel && cancel();
37 | return this.state;
38 | },
39 | },
40 | sagas: {
41 | *fragmentSaga({ payload }, { put, call }) {
42 | let data;
43 | if (isString(payload)) {
44 | //fragment为url的情况
45 | data = yield call(fetchFragment, payload);
46 | } else {
47 | //防止源数据被改动
48 | data = payload;
49 | }
50 | if (!data) {
51 | return;
52 | }
53 | const { total, fragments } = data;
54 | if (!total || !fragments) {
55 | return;
56 | }
57 | //需要做safari的日期格式兼容。
58 | const total_begin = +new Date(total.begin.replace(/-/g, '/')) / 1000;
59 | const total_end = +new Date(total.end.replace(/-/g, '/')) / 1000;
60 | const dataAfterAdapter = [];
61 | let duration = total_end - total_begin;
62 | let gaps = 0;
63 | fragments.forEach(obj => {
64 | if (!obj.begin || !obj.end) {
65 | return;
66 | }
67 | const obj_begin = +new Date(obj.begin.replace(/-/g, '/')) / 1000;
68 | const obj_end = +new Date(obj.end.replace(/-/g, '/')) / 1000;
69 | // duration中无视频的开始结束的时间,按秒算
70 | const begin = obj_begin - total_begin;
71 | const end = obj_end - total_begin;
72 | // duration 无视频的大小,按秒算
73 | const gap = obj_end - obj_begin;
74 | gaps += gap;
75 | dataAfterAdapter.push({
76 | gap,
77 | //包括前面的gap
78 | gaps,
79 | begin,
80 | end,
81 | });
82 | });
83 | let fragmentData = {};
84 | if (dataAfterAdapter.length > 0) {
85 | fragmentData = {
86 | duration,
87 | timeDuration: hms(duration),
88 | data: dataAfterAdapter,
89 | };
90 | }
91 | fragmentData.videoBeginDateTime = total_begin;
92 | yield put({
93 | type: 'fragmentReducer',
94 | payload: fragmentData,
95 | });
96 | },
97 | },
98 | };
99 | }
100 |
--------------------------------------------------------------------------------
/src/model/fullscreen.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'fullscreen';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | fullscreenReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *fullscreenSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'fullscreenReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/living.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'living';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | dataReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *setLivingSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'dataReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/loading.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'loading';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | loadingStateReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *setLoadingStateSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'loadingStateReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/muted.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'muted';
2 | export default function() {
3 | return {
4 | namespace,
5 | //默认不静音
6 | state: false,
7 | reducers: {
8 | dataReducer: function(state, { payload }) {
9 | return payload;
10 | },
11 | clear: function(state, { payload }) {
12 | return this.state;
13 | },
14 | },
15 | sagas: {
16 | *dataSaga({ payload }, { put }) {
17 | yield put({
18 | type: 'dataReducer',
19 | payload,
20 | });
21 | },
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/model/not-autoplay.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'not-autoplay';
2 | export default function() {
3 | return {
4 | namespace,
5 | //not-autoplay页面是否展示
6 | state: true,
7 | reducers: {
8 | notAutoPlayStateReducer: function(state, { payload }) {
9 | return payload;
10 | },
11 | clear: function(state, { payload }) {
12 | return this.state;
13 | },
14 | },
15 | sagas: {
16 | *notAutoPlayStateSaga({ payload }, { put }) {
17 | yield put({
18 | type: 'notAutoPlayStateReducer',
19 | payload,
20 | });
21 | },
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/model/picture-quality.js:
--------------------------------------------------------------------------------
1 | import { standardReducer } from '../utils/util';
2 |
3 | export const namespace = 'picture-quality';
4 | export default function() {
5 | return {
6 | namespace,
7 | state: {
8 | list: null,
9 | //-1为自动
10 | currentQuality: -1,
11 | },
12 | reducers: {
13 | dataReducer: standardReducer,
14 | clear: function(state, { payload }) {
15 | return this.state;
16 | },
17 | },
18 | sagas: {
19 | *dataSaga({ payload }, { put }) {
20 | yield put({
21 | type: 'dataReducer',
22 | payload,
23 | });
24 | },
25 | },
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/model/play-pause.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'play-pause';
2 | export default function() {
3 | return {
4 | namespace,
5 | //是否播放
6 | state: false,
7 | reducers: {
8 | playPauseReducer: function(state, { payload }) {
9 | return payload;
10 | },
11 | clear: function(state, { payload }) {
12 | return this.state;
13 | },
14 | },
15 | sagas: {
16 | *playPauseSaga({ payload }, { put }) {
17 | yield put({
18 | type: 'playPauseReducer',
19 | payload,
20 | });
21 | },
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/model/playback-rate.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'playback-rate';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: 1,
6 | reducers: {
7 | dataReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *setPlaybackRateSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'dataReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/ready.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'ready';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | state: function(state, { payload }) {
8 | return true;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/model/rotate.js:
--------------------------------------------------------------------------------
1 | export const namespace = 'rotate';
2 | export default function() {
3 | return {
4 | namespace,
5 | state: false,
6 | reducers: {
7 | dataReducer: function(state, { payload }) {
8 | return payload;
9 | },
10 | clear: function(state, { payload }) {
11 | return this.state;
12 | },
13 | },
14 | sagas: {
15 | *dataSaga({ payload }, { put }) {
16 | yield put({
17 | type: 'dataReducer',
18 | payload,
19 | });
20 | },
21 | },
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/model/selection.js:
--------------------------------------------------------------------------------
1 | // import { standardReducer } from '../utils/util';
2 | export const namespace = 'selection';
3 | export default function() {
4 | return {
5 | namespace,
6 | state: null,
7 | reducers: {
8 | dataReducer: function(state, action) {
9 | return action.payload;
10 | },
11 | clear: function(state, { payload }) {
12 | return this.state;
13 | },
14 | },
15 | sagas: {
16 | *dataSaga({ payload }, { put }) {
17 | yield put({
18 | type: 'dataReducer',
19 | payload,
20 | });
21 | },
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/model/time-slider.js:
--------------------------------------------------------------------------------
1 | import { standardReducer } from '../utils/util';
2 |
3 | export const namespace = 'time-slider';
4 | export default function() {
5 | return {
6 | namespace,
7 | state: {
8 | //最大值为1
9 | position: 0,
10 | buffer: 0,
11 | duration: 0,
12 | },
13 | reducers: {
14 | timeReducer: standardReducer,
15 | clear: function(state, { payload }) {
16 | return this.state;
17 | },
18 | },
19 | sagas: {
20 | *timeSaga(
21 | {
22 | payload: { currentTime, duration = 0, buffer },
23 | },
24 | { put }
25 | ) {
26 | let percent = currentTime / duration;
27 | // console.log(percent, currentTime, duration);
28 | if (percent > 1) {
29 | percent = 1;
30 | }
31 | if (isNaN(percent)) {
32 | percent = 0;
33 | }
34 | if (isNaN(duration)) {
35 | duration = 0;
36 | }
37 | yield put({
38 | type: 'timeReducer',
39 | payload: {
40 | percent,
41 | buffer: buffer / duration,
42 | duration,
43 | },
44 | });
45 | },
46 | },
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/src/model/time.js:
--------------------------------------------------------------------------------
1 | import { standardReducer, hms } from '../utils/util';
2 |
3 | export const namespace = 'time';
4 | export default function() {
5 | return {
6 | namespace,
7 | state: {
8 | currentTime: 0,
9 | elapse: '00:00',
10 | duration: '00:00',
11 | //原始video的duration,为了让其他地方使用,存一份
12 | secondDuration: 0,
13 | },
14 | reducers: {
15 | timeReducer: standardReducer,
16 | clear: function(state, { payload }) {
17 | return this.state;
18 | },
19 | },
20 | sagas: {
21 | *timeSaga(
22 | {
23 | payload: { currentTime, duration = 0 },
24 | },
25 | { put }
26 | ) {
27 | //currentTime和duration会有误差,最大的currentTime基本都比duration小,+0.25是为了减少time中的误差。
28 | const elapse = hms(Math.min(currentTime + 0.25, duration));
29 | const timeDuration = hms(duration);
30 | yield put({
31 | type: 'timeReducer',
32 | payload: {
33 | elapse,
34 | duration: timeDuration,
35 | secondDuration: duration,
36 | currentTime,
37 | },
38 | });
39 | },
40 | },
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/model/track.js:
--------------------------------------------------------------------------------
1 | import { standardReducer } from '../utils/util';
2 | import fetch from '../utils/fetch';
3 | import srtParser from '../utils/srt';
4 |
5 | //请求取消函数
6 | let cancel;
7 |
8 | function fetchTrack(url, params) {
9 | const cancelSource = fetch.getCancelSource();
10 | cancel = cancelSource.cancel;
11 | return fetch
12 | .get(url, { responseType: 'text', params, cancelToken: cancelSource.token })
13 | .catch(error => {
14 | return false;
15 | });
16 | }
17 |
18 | export const namespace = 'track';
19 | export default function() {
20 | //存放subtitle的cues
21 | const cuesList = [];
22 | return {
23 | namespace,
24 | state: {
25 | subtitleId: -1,
26 | subtitleList: [],
27 | subtitleCues: null,
28 | thumbnails: null,
29 | },
30 | reducers: {
31 | trackReducer: standardReducer,
32 | clear: function(state, { payload }) {
33 | cancel && cancel();
34 | return this.state;
35 | },
36 | },
37 | sagas: {
38 | *subtitleListSaga(
39 | {
40 | payload: { subtitleList, subtitleId },
41 | },
42 | { put, call }
43 | ) {
44 | const data = {
45 | subtitleId,
46 | };
47 | if (subtitleList) {
48 | data.subtitleList = subtitleList;
49 | }
50 | yield put({
51 | type: 'trackReducer',
52 | payload: data,
53 | });
54 | },
55 | *hlsSubtitleCuesSaga({ payload }, { put, call }) {
56 | yield put({
57 | type: 'trackReducer',
58 | payload: {
59 | subtitleCues: payload,
60 | },
61 | });
62 | },
63 | *subtitleCuesSaga({ payload }, { put, call }) {
64 | let data;
65 | if (payload.subtitleId === -1) {
66 | //关闭字幕
67 | data = [];
68 | } else if (!cuesList[payload.subtitleId]) {
69 | const vtt = yield call(fetchTrack, payload.file);
70 | if (!vtt) {
71 | return;
72 | }
73 | data = srtParser(vtt);
74 | } else {
75 | data = cuesList[payload.subtitleId];
76 | }
77 | if (data) {
78 | cuesList[payload.subtitleId] = data;
79 | }
80 | yield put({
81 | type: 'trackReducer',
82 | payload: {
83 | subtitleCues: data,
84 | },
85 | });
86 | },
87 | *thumbnailSaga({ payload }, { put, call }) {
88 | const vtt = yield call(fetchTrack, payload.file);
89 | if (!vtt) {
90 | return;
91 | }
92 | const data = srtParser(vtt);
93 | data.forEach(v => {
94 | let url = payload.file
95 | .split('?')[0]
96 | .split('/')
97 | .slice(0, -1)
98 | .join('/');
99 | if (~v.text.indexOf('://')) {
100 | url = v.text;
101 | } else {
102 | url += '/' + v.text;
103 | }
104 | if (~url.indexOf('#xywh')) {
105 | //一张图片中有多张缩略图
106 | let matched = /(.+)#xywh=(\d+),(\d+),(\d+),(\d+)/.exec(url);
107 | v.thumbnail = matched;
108 | } else {
109 | //单张图片直接做缩略图
110 | v.thumbnail = url;
111 | }
112 | });
113 | yield put({
114 | type: 'trackReducer',
115 | payload: {
116 | thumbnails: data,
117 | },
118 | });
119 | },
120 | },
121 | };
122 | }
123 |
--------------------------------------------------------------------------------
/src/model/video/events/hlsjs.js:
--------------------------------------------------------------------------------
1 | import { namespace as videoNamespace } from '../../video';
2 | import * as logger from '../../../utils/logger';
3 |
4 | class HlsjsEvents {
5 | constructor(payload) {
6 | this.api = payload.api;
7 | this.dispatch = payload.dispatch;
8 | this.config = payload.config;
9 | if (!this.api.hlsObj) {
10 | logger.warn('No hls.js instantiation.');
11 | return;
12 | }
13 | this.loaded();
14 | logger.info('Listening:', 'listening on some hls.js events.');
15 | }
16 | loaded() {
17 | const api = this.api;
18 | api.hlsObj.on(api.Hls.Events.MANIFEST_LOADED, (event, data) => {
19 | this.setSubtitle();
20 | this.setQuality();
21 | });
22 | }
23 | setQuality() {
24 | const api = this.api;
25 | const dispatch = this.dispatch;
26 | const list = [];
27 | api.hlsObj.levels.forEach((v, k) => {
28 | if (v.bitrate) {
29 | list.push({
30 | label: `${v.height}p`,
31 | value: k,
32 | });
33 | }
34 | });
35 | if (list[0]) {
36 | dispatch({
37 | type: `${videoNamespace}/pictureQualityList`,
38 | payload: {
39 | list,
40 | },
41 | });
42 | }
43 | }
44 | setSubtitle() {
45 | const api = this.api;
46 | const dispatch = this.dispatch;
47 | // console.log(api.hlsObj.subtitleTracks);
48 | let clearIntervalObj;
49 | api.hlsObj.on(api.Hls.Events.SUBTITLE_TRACK_SWITCH, (event, data) => {
50 | const currentTextTrack = api.textTracks[data.id];
51 | let cues = [];
52 | if (currentTextTrack) {
53 | //vtt没有加载时,切换是没信息的,所以首次切换字幕这里是不会触发dispatch
54 | cues = currentTextTrack.cues;
55 | }
56 | logger.info('Subtitle switched');
57 | dispatch({
58 | type: `${videoNamespace}/hlsSubtitleCues`,
59 | payload: cues,
60 | });
61 | });
62 | api.hlsObj.on(api.Hls.Events.SUBTITLE_TRACK_LOADED, (event, data) => {
63 | //首次切换字幕,加载字幕才会触发这个事件,第二次切换不会触发。
64 | const currentTextTrack = api.textTracks[api.currentSubtitleTrack];
65 | if (!currentTextTrack) {
66 | return;
67 | }
68 | //防止切换过快没清除。
69 | clearInterval(clearIntervalObj);
70 | let tempLength = 0;
71 | let k = 0;
72 | clearIntervalObj = setInterval(function() {
73 | const length = currentTextTrack.cues.length;
74 | if (currentTextTrack.cues.length > 0 && tempLength !== length) {
75 | tempLength = length;
76 | dispatch({
77 | type: `${videoNamespace}/hlsSubtitleCues`,
78 | payload: currentTextTrack.cues,
79 | });
80 | } else {
81 | //因为是异步的无法确定什么时候加载完字幕,字幕也可能分几个片段加载的。
82 | if (k >= 2) {
83 | logger.info('Subtitle switched');
84 | //如果两次以上length都没变化就判断为,加载完成。
85 | //这里不排除网络很差的导致加载字幕出问题,但是这种极端情况,不好处理,也没必要处理。
86 | //因为如果网络都差到连2KB的内容都加载不了,也完全播放不了视频了。
87 | clearInterval(clearIntervalObj);
88 | }
89 | k++;
90 | }
91 | }, 200);
92 | });
93 | dispatch({
94 | type: `${videoNamespace}/subtitleList`,
95 | payload: {
96 | subtitleList: api.hlsObj.subtitleTracks,
97 | subtitleId: -1,
98 | },
99 | });
100 | }
101 | }
102 |
103 | export default function(payload) {
104 | return new HlsjsEvents(payload);
105 | }
106 |
--------------------------------------------------------------------------------
/src/model/volume.js:
--------------------------------------------------------------------------------
1 | // import { standardReducer } from '../utils/util';
2 |
3 | export const namespace = 'volume';
4 | export default function() {
5 | return {
6 | namespace,
7 | //100为自定义的总音量,原生的最大为1
8 | //先设置声音为20,太大可能一开始就造成声音过多,用户体验不好。
9 | state: 20,
10 | reducers: {
11 | dataReducer: function(state, { payload }) {
12 | return payload;
13 | },
14 | clear: function(state, { payload }) {
15 | return this.state;
16 | },
17 | },
18 | sagas: {
19 | *dataSaga({ payload }, { put }) {
20 | if (payload && isNaN(payload)) {
21 | //兼容volume不为数字的情况
22 | payload = 0;
23 | }
24 | yield put({
25 | type: 'dataReducer',
26 | payload,
27 | });
28 | },
29 | },
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/playlist.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import isString from 'lodash/isString';
5 | //内部依赖包
6 | import Player from './index';
7 | import Carousel from './view/components/carousel';
8 |
9 | export default class Playlist extends React.Component {
10 | static propTypes = {
11 | playlist: PropTypes.array.isRequired,
12 | //当前选择播放的视频源(播放列表中的某项)
13 | activeItem: PropTypes.number,
14 | //视频走定时轮播,没有默认值
15 | //可以使毫秒设置轮播间隔
16 | videoCarousel: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
17 | };
18 | static childContextTypes = {
19 | playlist: PropTypes.array,
20 | activeItem: PropTypes.number,
21 | setActiveItem: PropTypes.func,
22 | };
23 | getChildContext() {
24 | return {
25 | playlist: this.props.playlist,
26 | activeItem: this.activeItem,
27 | setActiveItem: this.setActiveItem,
28 | };
29 | }
30 | displayName = 'Playlist';
31 | state = {
32 | activeItem: this.props.activeItem || 1,
33 | };
34 | componentDidMount() {
35 | this.setVideoSwitchInterval();
36 | }
37 | componentWillUnmount() {
38 | clearInterval(this.clearInterval);
39 | this.clearInterval = null;
40 | }
41 | setVideoSwitchInterval = () => {
42 | const { playlist, videoCarousel } = this.props;
43 | if (videoCarousel) {
44 | clearInterval(this.clearInterval);
45 | let time = 1000 * 10;
46 | if (!isNaN(+JSON.stringify(videoCarousel))) {
47 | time = videoCarousel;
48 | }
49 | this.clearInterval = setInterval(() => {
50 | if (this.activeItem >= playlist.length) {
51 | this.activeItem = 1;
52 | } else {
53 | this.activeItem = this.activeItem + 1;
54 | }
55 | }, time);
56 | }
57 | };
58 | get activeItem() {
59 | return this.state.activeItem;
60 | }
61 | set activeItem(value) {
62 | this.setState({ activeItem: value });
63 | }
64 | setActiveItem = value => {
65 | this.activeItem = value;
66 | };
67 | onPlaylistItemClick = index => {
68 | return e => {
69 | this.setVideoSwitchInterval();
70 | this.setState({ activeItem: index + 1 });
71 | };
72 | };
73 | render() {
74 | const { title, gap, showCount, playlist = [], ...other } = this.props;
75 | const { activeItem } = this.state;
76 | const playerProps = {
77 | ...other,
78 | ...playlist[activeItem - 1],
79 | activeItem,
80 | title: (
81 |
82 | {playlist[activeItem - 1].title}
83 | {title}
84 |
85 | ),
86 | };
87 | return (
88 |
89 |
90 | {playlist[0] && (
91 |
92 |
98 | {playlist &&
99 | playlist.map((v, k) => {
100 | return (
101 |
102 |
103 | {React.isValidElement(v.cover) && v.cover}
104 | {isString(v.cover) &&

}
105 |
106 |
107 | {v.title}
108 |
109 |
110 | );
111 | })}
112 |
113 |
114 | )}
115 |
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/provider.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import './version';
6 | import Provider from './libs/provider/saga-model-provider';
7 | import modelList from './model-list';
8 | //icon的js
9 | import './assets/icon/iconfont';
10 |
11 | class ModelRegister extends React.Component {
12 | static contextTypes = {
13 | sagaStore: PropTypes.object,
14 | };
15 | displayName = 'ModelRegister';
16 | state = {};
17 | registerModel = register => {
18 | const allModelPromise = modelList.map(modelId => {
19 | const model = require(`./model/${modelId}.js`).default;
20 | return model;
21 | });
22 | return Promise.all(allModelPromise).then(models => {
23 | models.forEach(m => {
24 | register(m);
25 | });
26 | });
27 | };
28 | componentDidMount() {
29 | this.registerModel(this.context.sagaStore.register).then(() => {
30 | this.setState({
31 | canBeRendered: true,
32 | });
33 | });
34 | }
35 | render() {
36 | const { children } = this.props;
37 | if (this.state.canBeRendered) {
38 | return {children};
39 | } else {
40 | return false;
41 | }
42 | }
43 | }
44 | export default function player(props) {
45 | return (
46 | {
51 | console.error(error);
52 | },
53 | },
54 | ]}
55 | >
56 | {props.children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/style.js:
--------------------------------------------------------------------------------
1 | import './assets/css/common.less';
2 |
--------------------------------------------------------------------------------
/src/umd.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Player from './';
4 | import './style';
5 | window.React = React;
6 | window.ReactDOM = ReactDOM;
7 |
8 | //兼容preact用法
9 | let root;
10 | /**
11 | *@param {object} props 播放器配置
12 | *@param {function} callback 播放器实例化后的回调函数,返回播放器实例
13 | * 也可以使用promise获取
14 | *@return {promise} 播放器实例
15 | */
16 | function player(props, callback) {
17 | const { id, children, ...other } = props;
18 | return new Promise(resolve => {
19 | root = ReactDOM.render(
20 | {
22 | callback && callback(player);
23 | const remove = player.remove;
24 | player.remove = function() {
25 | //如果有定义remove先运行。
26 | remove && remove();
27 | root = ReactDOM.render(false, document.getElementById(id), root);
28 | };
29 | resolve(player);
30 | }}
31 | {...other}
32 | >
33 | {children}
34 | ,
35 | document.getElementById(id),
36 | root
37 | );
38 | });
39 | }
40 |
41 | export default player;
42 |
--------------------------------------------------------------------------------
/src/utils/browser.js:
--------------------------------------------------------------------------------
1 | const userAgent = navigator.userAgent;
2 |
3 | function userAgentMatch(regex) {
4 | return userAgent.match(regex) !== null;
5 | }
6 |
7 | function lazyUserAgentMatch(regex) {
8 | return function() {
9 | return userAgentMatch(regex);
10 | };
11 | }
12 |
13 | export const isFF = lazyUserAgentMatch(/gecko\//i);
14 | export const isIETrident = lazyUserAgentMatch(/trident\/.+rv:\s*11/i);
15 | // export const isIPod = lazyUserAgentMatch(/iP(hone|od)/i);
16 | // export const isIPad = lazyUserAgentMatch(/iPad/i);
17 | // export const isOSX = lazyUserAgentMatch(/Macintosh/i);
18 | // Check for Facebook App Version to see if it's Facebook
19 | // export const isFacebook = lazyUserAgentMatch(/FBAV/i);
20 |
21 | export function isEdge() {
22 | return userAgentMatch(/\sEdge\/\d+/i);
23 | }
24 |
25 | export function isMSIE() {
26 | return userAgentMatch(/msie/i);
27 | }
28 |
29 | export function isChrome() {
30 | return userAgentMatch(/\s(?:Chrome|CriOS)\//i) && !isEdge();
31 | }
32 |
33 | export function isIE() {
34 | return isEdge() || isIETrident() || isMSIE();
35 | }
36 |
37 | export function isSafari() {
38 | return (
39 | userAgentMatch(/safari/i) &&
40 | !userAgentMatch(/(?:Chrome|CriOS|chromium|android)/i)
41 | );
42 | }
43 |
44 | // /** Matches iOS devices **/
45 | // export function isIOS() {
46 | // return userAgentMatch(/iP(hone|ad|od)/i);
47 | // }
48 | //
49 | // /** Matches Android devices **/
50 | // export function isAndroidNative() {
51 | // // Android Browser appears to include a user-agent string for Chrome/18
52 | // if (userAgentMatch(/chrome\/[123456789]/i) && !userAgentMatch(/chrome\/18/)) {
53 | // return false;
54 | // }
55 | // return isAndroid();
56 | // }
57 | //
58 | // export function isAndroid() {
59 | // return userAgentMatch(/Android/i);
60 | // }
61 | //
62 | // /** Matches iOS and Android devices **/
63 | // export function isMobile() {
64 | // return isIOS() || isAndroid();
65 | // }
66 | //
67 | // export function isIframe() {
68 | // try {
69 | // return window.self !== window.top;
70 | // } catch (e) {
71 | // return true;
72 | // }
73 | // }
74 |
--------------------------------------------------------------------------------
/src/utils/const.js:
--------------------------------------------------------------------------------
1 | import * as storage from './storage';
2 |
3 | //参考http://www.w3school.com.cn/tags/av_prop_readystate.asp
4 | //没有关于音频/视频是否就绪的信息
5 | export const HAVE_NOTHING = 0;
6 | //关于音频/视频就绪的元数据
7 | export const HAVE_METADATA = 1;
8 | //关于当前播放位置的数据是可用的,但没有足够的数据来播放下一帧/毫秒
9 | //timeupdate事件中可以处理状态,当做loading
10 | export const HAVE_CURRENT_DATA = 2;
11 | //当前及至少下一帧的数据是可用的
12 | export const HAVE_FUTURE_DATA = 3;
13 | //可用数据足以开始播放
14 | export const HAVE_ENOUGH_DATA = 4;
15 | //自定义的最大声音值
16 | export const MAX_VOLUME = 100;
17 |
18 | export const DEBUG = storage.get('debug');
19 | //视频超时,超时后会超时reload,尝试3次后,报错误信息。
20 | export const VIDEO_TIMEOUT = 1000 * 10;
21 | //默认的播放器纵横比
22 | export const ASPECT_RATIO = '16:9';
23 | //用户不活跃的时候,controlbar即将消失的时间
24 | export const CONTROLBAR_HIDE_TIME = 2000;
25 | export const DEFAULT_PLAYBACKRATES = [1, 1.25, 1.5, 1.75, 2];
26 | //直播最大缓存
27 | export const LIVING_MAXBUFFER_TIME = 6;
28 | //延时展示loading的时间
29 | export const SHOW_LOADING_LAZY_TIME = 500;
30 | //延时展示错误信息的时间
31 | export const SHOW_ERROR_MESSAGE_LAZY_TIME = 500;
32 | //尝试重连次数
33 | export const RETRY_TIMES = 5;
34 |
--------------------------------------------------------------------------------
/src/utils/dom/addEventListener.js:
--------------------------------------------------------------------------------
1 | import addDOMEventListener from 'add-dom-event-listener';
2 | import ReactDOM from 'react-dom';
3 |
4 | export default function addEventListenerWrap(target, eventType, cb) {
5 | /* eslint camelcase: 2 */
6 | const callback = ReactDOM.unstable_batchedUpdates
7 | ? function run(e) {
8 | ReactDOM.unstable_batchedUpdates(cb, e);
9 | }
10 | : cb;
11 | return addDOMEventListener(target, eventType, callback);
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/dom/contains.js:
--------------------------------------------------------------------------------
1 | export default function contains(root, n) {
2 | let node = n;
3 | while (node) {
4 | if (node === root) {
5 | return true;
6 | }
7 | node = node.parentNode;
8 | }
9 |
10 | return false;
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/dom/fullscreen.js:
--------------------------------------------------------------------------------
1 | const DOCUMENT_FULLSCREEN_EVENTS = [
2 | 'fullscreenchange',
3 | 'webkitfullscreenchange',
4 | 'mozfullscreenchange',
5 | 'MSFullscreenChange',
6 | ];
7 |
8 | export default function(elementContext, documentContext, changeCallback) {
9 | const requestFullscreen =
10 | elementContext.requestFullscreen ||
11 | elementContext.webkitRequestFullscreen ||
12 | elementContext.webkitRequestFullScreen ||
13 | elementContext.mozRequestFullScreen ||
14 | elementContext.msRequestFullscreen;
15 |
16 | const exitFullscreen =
17 | documentContext.exitFullscreen ||
18 | documentContext.webkitExitFullscreen ||
19 | documentContext.webkitCancelFullScreen ||
20 | documentContext.mozCancelFullScreen ||
21 | documentContext.msExitFullscreen;
22 |
23 | const supportsDomFullscreen = !!(requestFullscreen && exitFullscreen);
24 |
25 | for (let i = DOCUMENT_FULLSCREEN_EVENTS.length; i--; ) {
26 | documentContext.addEventListener(
27 | DOCUMENT_FULLSCREEN_EVENTS[i],
28 | changeCallback
29 | );
30 | }
31 |
32 | return {
33 | events: DOCUMENT_FULLSCREEN_EVENTS,
34 | supportsDomFullscreen: function() {
35 | return supportsDomFullscreen;
36 | },
37 | requestFullscreen: function() {
38 | requestFullscreen.apply(elementContext);
39 | },
40 | exitFullscreen: function() {
41 | exitFullscreen.apply(documentContext);
42 | },
43 | fullscreenElement: function() {
44 | return (
45 | documentContext.fullscreenElement ||
46 | documentContext.webkitCurrentFullScreenElement ||
47 | documentContext.mozFullScreenElement ||
48 | documentContext.msFullscreenElement
49 | );
50 | },
51 | remove: function() {
52 | for (let i = DOCUMENT_FULLSCREEN_EVENTS.length; i--; ) {
53 | documentContext.removeEventListener(
54 | DOCUMENT_FULLSCREEN_EVENTS[i],
55 | changeCallback
56 | );
57 | }
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/error-code.js:
--------------------------------------------------------------------------------
1 | export const TIMEOUT_ERROR = 'timeoutError';
2 | export const LOAD_ERROR = 'loadError';
3 | export const CONTENT_PARSING_ERROR = 'contentParsingError';
4 | export const UNKNOWN_ERROR = 'unknownError';
5 | //致命错误
6 | export const FATAL_ERROR = 'unknownError';
7 |
--------------------------------------------------------------------------------
/src/utils/fetch.js:
--------------------------------------------------------------------------------
1 | import fetch from '../libs/fetch';
2 |
3 | export default fetch();
4 |
--------------------------------------------------------------------------------
/src/utils/logger/index.js:
--------------------------------------------------------------------------------
1 | import { DEBUG } from '../const';
2 | export default function logger(type) {
3 | const originType = type;
4 | function noop() {}
5 | if (!DEBUG) {
6 | return noop;
7 | }
8 | if (!window.console) {
9 | return noop;
10 | }
11 | if (!window.console[type]) {
12 | if (window.console.log) {
13 | type = 'log';
14 | } else {
15 | return noop;
16 | }
17 | }
18 | function log(title, ...args) {
19 | const prevArg = [];
20 | if (
21 | Object.prototype.toString.apply(title) === '[object String]' ||
22 | Object.prototype.toString.apply(title) === '[object Number]'
23 | ) {
24 | const fontSize = 'font-size: 14px;';
25 | prevArg.push(`%c${title}%c`);
26 | if (originType === 'info') {
27 | prevArg.push(fontSize + 'font-weight: bold;color: #108ee9;');
28 | } else if (originType === 'success') {
29 | prevArg.push(fontSize + 'font-weight: bold;color: green;');
30 | } else {
31 | prevArg.push(fontSize + 'font-weight: bold');
32 | }
33 | prevArg.push('');
34 | } else {
35 | prevArg.push(title);
36 | }
37 | window.console[type](...prevArg, ...args);
38 | }
39 | return log;
40 | }
41 |
42 | export const log = logger('log');
43 | export const success = logger('success');
44 | export const info = logger('info');
45 | export const error = logger('error');
46 | export const warn = logger('warn');
47 |
--------------------------------------------------------------------------------
/src/utils/srt.js:
--------------------------------------------------------------------------------
1 | import { seconds, trim } from './util';
2 |
3 | // Component that loads and parses an SRT file
4 |
5 | export default function Srt(data) {
6 | // Trim whitespace and split the list by returns.
7 | var _captions = [];
8 | data = trim(data);
9 | var list = data.split('\r\n\r\n');
10 | if (list.length === 1) {
11 | list = data.split('\n\n');
12 | }
13 |
14 | for (var i = 0; i < list.length; i++) {
15 | if (list[i] === 'WEBVTT') {
16 | continue;
17 | }
18 | // Parse each entry
19 | var entry = _entry(list[i]);
20 | if (entry.text) {
21 | _captions.push(entry);
22 | }
23 | }
24 |
25 | return _captions;
26 | }
27 |
28 | /* Parse a single captions entry. */
29 | function _entry(data) {
30 | var entry = {};
31 | var array = data.split('\r\n');
32 | if (array.length === 1) {
33 | array = data.split('\n');
34 | }
35 | var idx = 1;
36 | if (array[0].indexOf(' --> ') > 0) {
37 | idx = 0;
38 | }
39 | if (array.length > idx + 1 && array[idx + 1]) {
40 | // This line contains the start and end.
41 | var line = array[idx];
42 | var index = line.indexOf(' --> ');
43 | if (index > 0) {
44 | entry.begin = seconds(line.substr(0, index));
45 | entry.end = seconds(line.substr(index + 5));
46 | // Remaining lines contain the text
47 | entry.text = array.slice(idx + 1).join('\r\n');
48 | }
49 | }
50 | return entry;
51 | }
52 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | let storage;
2 | const namespace = 'html5-player-';
3 | try {
4 | storage = window.localStorage;
5 | } catch (e) {
6 | /* ignore */
7 | }
8 |
9 | export function set(name, val) {
10 | storage.setItem(namespace + name, val);
11 | }
12 | export function get(name) {
13 | let val = storage.getItem(namespace + name);
14 | if (val === 'false' || val === 'true') {
15 | //处理boolean值
16 | val = JSON.parse(val);
17 | } else if (!isNaN(val) && val !== null && val !== undefined) {
18 | //处理数字
19 | val = parseInt(val, 10);
20 | }
21 | return val;
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/video.js:
--------------------------------------------------------------------------------
1 | //为了使用一些video api,但是个video不会在页面上渲染。
2 | const video = document.createElement('video');
3 | export default video;
4 |
--------------------------------------------------------------------------------
/src/version.js:
--------------------------------------------------------------------------------
1 | window.html5PlayerVersion = '0.5.10';
2 |
--------------------------------------------------------------------------------
/src/view/components/contextmenu.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import PropTypes from 'prop-types';
5 | // import classnames from 'classnames';
6 | //内部依赖包
7 | import addEventListener from '../../utils/dom/addEventListener';
8 | import contains from '../../utils/dom/contains';
9 | import { cloneElement } from '../../utils/util';
10 |
11 | export default class ContextMenu extends React.Component {
12 | displayName = 'ContextMenu';
13 | static propTypes = {
14 | content: PropTypes.element,
15 | //overflow 是否不能超出容器的边界,默认可以超出
16 | overflow: PropTypes.bool,
17 | };
18 | state = { showMenu: false };
19 | renderContextmenu() {
20 | const { content } = this.props;
21 | const { showMenu, left, top } = this.state;
22 | return cloneElement(content, {
23 | key: 'menu',
24 | ref: 'menu',
25 | style: {
26 | visibility: showMenu ? 'visible' : 'hidden',
27 | color: 'red',
28 | position: 'absolute',
29 | whiteSpace: 'nowrap',
30 | zIndex: 100000,
31 | left,
32 | top,
33 | },
34 | });
35 | }
36 | onContextMenu = e => {
37 | e.preventDefault();
38 | //overflow 是否不能超出容器的边界
39 | const { overflow = true } = this.props;
40 | const containerTarget = ReactDOM.findDOMNode(this.refs.containerTarget);
41 | const menuDOM = ReactDOM.findDOMNode(this.refs.menu);
42 | const body = containerTarget.ownerDocument.body;
43 | const containerTargetRect = containerTarget.getBoundingClientRect();
44 | const menuDOMRect = menuDOM.getBoundingClientRect();
45 | let left = parseInt(`${e.pageX - containerTargetRect.left}`, 10);
46 | let top = parseInt(`${e.pageY - containerTargetRect.top}`, 10);
47 | // console.log(body.clientWidth, left, e.pageX);
48 | if (!overflow) {
49 | //overflow 是否不能超出容器的边界
50 | if (left > containerTargetRect.width - menuDOMRect.width) {
51 | //超过container右边
52 | left = containerTargetRect.width - menuDOMRect.width;
53 | }
54 | if (top > containerTargetRect.height - menuDOMRect.height) {
55 | //超过container右边
56 | top = containerTargetRect.height - menuDOMRect.height;
57 | }
58 | } else {
59 | if (e.pageX > body.clientWidth - menuDOMRect.width) {
60 | //超过浏览器右边
61 | left = body.clientWidth - menuDOMRect.width - containerTargetRect.left;
62 | }
63 | if (e.pageY > body.clientHeight - menuDOMRect.height) {
64 | //超过浏览器底部
65 | top = body.clientHeight - menuDOMRect.height - containerTargetRect.top;
66 | }
67 | }
68 | this.setState({
69 | showMenu: true,
70 | left: `${left}px`,
71 | top: `${top}px`,
72 | });
73 | this.documentMousedownEvent && this.documentMousedownEvent.remove();
74 | this.documentMousedownEvent = addEventListener(
75 | containerTarget.ownerDocument,
76 | 'mousedown',
77 | e => {
78 | if (!contains(menuDOM, e.target)) {
79 | this.documentMousedownEvent.remove();
80 | this.setState({
81 | showMenu: false,
82 | });
83 | }
84 | }
85 | );
86 | };
87 | render() {
88 | const { children } = this.props;
89 | return cloneElement(
90 | children,
91 | {
92 | ref: 'containerTarget',
93 | onContextMenu: this.onContextMenu,
94 | },
95 | this.renderContextmenu()
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/view/contextmenu.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import classnames from 'classnames';
6 | import isArray from 'lodash/isArray';
7 | //内部依赖包
8 |
9 | export default class ContextMenu extends React.Component {
10 | displayName = 'ContextMenu';
11 | render() {
12 | const { content, className, ...other } = this.props;
13 | if (content === true) {
14 | return (
15 |
19 | {window.html5PlayerVersion && (
20 | - Html5 Player v{window.html5PlayerVersion}
21 | )}
22 | {!window.html5PlayerVersion && - Html5 Player
}
23 |
24 | );
25 | } else if (React.isValidElement(content)) {
26 | return content;
27 | } else if (isArray(content)) {
28 | return (
29 |
33 | {content.map((v, k) => {
34 | return - {v}
;
35 | })}
36 |
37 | );
38 | } else {
39 | return false;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/view/controlbar/capture.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import downloadjs from 'downloadjs';
5 | //内部依赖包
6 | import { randomKey } from '../../utils/util';
7 |
8 | export default class Capture extends React.Component {
9 | static propTypes = {};
10 | displayName = 'Capture';
11 | static contextTypes = {
12 | playerDOM: PropTypes.object,
13 | };
14 | captureName = () => {
15 | return `capture${randomKey()}.png`;
16 | };
17 | capture = e => {
18 | const video = this.context.playerDOM;
19 | const canvas = document.createElement('canvas');
20 | canvas.width = video.clientWidth;
21 | canvas.height = video.clientHeight;
22 | let ctx = canvas.getContext('2d');
23 | ctx.drawImage(video, 0, 0, video.clientWidth, video.clientHeight);
24 | downloadjs(canvas.toDataURL(), this.captureName(), 'image/png');
25 | };
26 | render() {
27 | return (
28 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/view/controlbar/full-off-screen.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from '../decorator/clear';
9 | import { namespace as videoNamespace } from '../../model/video';
10 | import { namespace as fullscreenStateNamespace } from '../../model/fullscreen';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | return {
17 | isfull: state[fullscreenStateNamespace],
18 | };
19 | })
20 | @clearDecorator([fullscreenStateNamespace])
21 | export default class FullOffScreen extends React.Component {
22 | //这里的配置参考jw-player的api
23 | static propTypes = {};
24 | displayName = 'FullOffScreen';
25 | state = {};
26 | dispatch = this.props.dispatch;
27 | onFullStateChange = e => {
28 | e.stopPropagation();
29 | const { isfull } = this.props;
30 | this.dispatch({
31 | type: `${videoNamespace}/fullscreen`,
32 | payload: !isfull,
33 | });
34 | };
35 | render() {
36 | const { isfull } = this.props;
37 | return (
38 |
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/view/controlbar/next.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | import PropTypes from 'prop-types';
5 | import classnames from 'classnames';
6 | //内部依赖包
7 |
8 | export default class Next extends React.Component {
9 | static contextTypes = {
10 | playlist: PropTypes.array,
11 | activeItem: PropTypes.number,
12 | setActiveItem: PropTypes.func,
13 | };
14 | displayName = 'Next';
15 | onClick = e => {
16 | this.context.setActiveItem(this.context.activeItem + 1);
17 | };
18 | render() {
19 | const { activeItem, playlist } = this.context;
20 | if (!playlist || !playlist[0]) {
21 | return false;
22 | }
23 | if (activeItem >= playlist.length) {
24 | return false;
25 | }
26 | return (
27 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/view/controlbar/picture-quality.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | // import classnames from 'classnames';
7 | //内部依赖包
8 | import Tooltip from '../components/tooltip';
9 | import clearDecorator from '../decorator/clear';
10 | import PictureQualityList from './setting/picture-quality-list';
11 | import { namespace as qualityNamespace } from '../../model/picture-quality';
12 |
13 | @connect(state => {
14 | return {
15 | qualityList: state[qualityNamespace].list,
16 | };
17 | })
18 | @clearDecorator([qualityNamespace])
19 | export default class PictureQuality extends React.Component {
20 | displayName = 'PictureQuality';
21 | state = {};
22 | onSelect = value => {
23 | this.setState({
24 | tooltipKey: value,
25 | });
26 | };
27 | renderContent() {
28 | return ;
29 | }
30 | render() {
31 | const { qualityList, locale } = this.props;
32 | const { tooltipKey } = this.state;
33 | if (!qualityList) {
34 | return false;
35 | }
36 | return (
37 |
43 |
44 | {locale.pictureQuality}
45 |
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/view/controlbar/play-pause.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from '../decorator/clear';
9 | import { namespace as videoNamespace } from '../../model/video';
10 | import { namespace as playPauseNamespace } from '../../model/play-pause';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | return {
17 | playing: state[playPauseNamespace],
18 | };
19 | })
20 | @clearDecorator([playPauseNamespace])
21 | export default class PlayPause extends React.Component {
22 | static propTypes = {};
23 | displayName = 'PlayPause';
24 | state = {};
25 | dispatch = this.props.dispatch;
26 | play = e => {
27 | e.stopPropagation();
28 | this.dispatch({
29 | type: `${videoNamespace}/play`,
30 | payload: {
31 | noControlbarAction: true,
32 | },
33 | });
34 | };
35 | pause = e => {
36 | e.stopPropagation();
37 | this.dispatch({
38 | type: `${videoNamespace}/pause`,
39 | });
40 | };
41 | render() {
42 | const { playing, living } = this.props;
43 | if (living) {
44 | return false;
45 | }
46 | return (
47 |
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/view/controlbar/playback-rate.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | // import classnames from 'classnames';
7 | //内部依赖包
8 | import Tooltip from '../components/tooltip';
9 | import clearDecorator from '../decorator/clear';
10 | import PlaybackRateList from './setting/playback-rate-list';
11 | import { namespace as playbackRateNamespace } from '../../model/playback-rate';
12 | import { namespace as livingNamespace } from '../../model/living';
13 |
14 | @connect(state => {
15 | return {
16 | playbackRate: state[playbackRateNamespace],
17 | living: state[livingNamespace],
18 | };
19 | })
20 | @clearDecorator([playbackRateNamespace])
21 | export default class PlaybackRate extends React.Component {
22 | displayName = 'PlaybackRate';
23 | state = {};
24 | onRatteSelect = rate => {
25 | this.setState({
26 | tooltipKey: rate,
27 | });
28 | };
29 | renderContent() {
30 | return ;
31 | }
32 | render() {
33 | const { playbackRate, locale } = this.props;
34 | const { tooltipKey } = this.state;
35 | return (
36 |
42 |
43 | {playbackRate + locale.speed}
44 |
45 |
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/view/controlbar/prev.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | import PropTypes from 'prop-types';
5 | import classnames from 'classnames';
6 | //内部依赖包
7 |
8 | export default class Prev extends React.Component {
9 | static contextTypes = {
10 | playlist: PropTypes.array,
11 | activeItem: PropTypes.number,
12 | setActiveItem: PropTypes.func,
13 | };
14 | displayName = 'Prev';
15 | onClick = e => {
16 | this.context.setActiveItem(this.context.activeItem - 1);
17 | };
18 | render() {
19 | const { activeItem, playlist } = this.context;
20 | if (!playlist || !playlist[0]) {
21 | return false;
22 | }
23 | if (activeItem <= 1) {
24 | return false;
25 | }
26 | return (
27 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/view/controlbar/rotate.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from '../decorator/clear';
9 | import { namespace as videoNamespace } from '../../model/video';
10 | import { namespace as rotateNamespace } from '../../model/rotate';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | return {
17 | rotate: state[rotateNamespace],
18 | };
19 | })
20 | @clearDecorator([rotateNamespace])
21 | export default class PlayPause extends React.Component {
22 | static propTypes = {};
23 | displayName = 'PlayPause';
24 | state = {};
25 | dispatch = this.props.dispatch;
26 | rotate = e => {
27 | e.stopPropagation();
28 | let { rotate } = this.props;
29 | if (rotate === 360) {
30 | rotate = 0;
31 | }
32 | this.dispatch({
33 | type: `${videoNamespace}/rotate`,
34 | payload: rotate + 90,
35 | });
36 | };
37 | render() {
38 | return (
39 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/view/controlbar/setting/list.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import { namespace as videoNamespace } from '../../../model/video';
6 |
7 | export default class List extends React.Component {
8 | static contextTypes = {
9 | localization: PropTypes.object,
10 | playerDOM: PropTypes.object,
11 | controlbarHideTime: PropTypes.number,
12 | };
13 | displayName = 'List';
14 | dispatch = this.props.dispatch;
15 | onSelectEvent = (value, callback) => {
16 | return e => {
17 | const { onSelect } = this.props;
18 | onSelect && onSelect(value, e);
19 | callback && callback();
20 | if (!this.context.playerDOM.paused) {
21 | //播放才处理,选择的时候因为鼠标位置已经离开了controlbar
22 | //所有选择完成需要,触发隐藏controlbar
23 | this.dispatch({
24 | type: `${videoNamespace}/controlbar`,
25 | payload: false,
26 | delayTime: this.context.controlbarHideTime,
27 | onControlbarEnter: false,
28 | });
29 | }
30 | };
31 | };
32 | getLocale() {
33 | return this.context.localization;
34 | }
35 | renderBack(title) {
36 | const { onBackEvent } = this.props;
37 | if (!onBackEvent) {
38 | return false;
39 | } else {
40 | return (
41 |
42 |
48 | {title}
49 |
50 | );
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/view/controlbar/setting/picture-quality-list.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import classnames from 'classnames';
5 | //内部依赖包
6 | import List from './list';
7 | import { namespace as videoNamespace } from '../../../model/video';
8 | import { namespace as qualityNamespace } from '../../../model/picture-quality';
9 |
10 | @connect(state => {
11 | return {
12 | qualityList: state[qualityNamespace].list,
13 | currentQuality: state[qualityNamespace].currentQuality,
14 | };
15 | })
16 | export default class PictureQualityList extends List {
17 | displayName = 'PictureQualityList';
18 | onSelect = value => {
19 | return this.onSelectEvent(value, () => {
20 | this.dispatch({
21 | type: `${videoNamespace}/switchPictureQuality`,
22 | payload: value,
23 | });
24 | });
25 | };
26 | render() {
27 | const { qualityList, currentQuality } = this.props;
28 | const locale = this.getLocale();
29 | return (
30 |
31 | {this.renderBack(locale.pictureQuality)}
32 | {qualityList &&
33 | qualityList.map(v => {
34 | const className = classnames({
35 | 'html5-player-list-selected': currentQuality === v.value,
36 | });
37 | return (
38 | -
43 | {v.label}
44 |
45 | );
46 | })}
47 | -
54 | {locale.autoQuality}
55 |
56 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/view/controlbar/setting/playback-rate-list.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import { connect } from 'react-redux';
4 | import classnames from 'classnames';
5 | //内部依赖包
6 | import List from './list';
7 | import { namespace as videoNamespace } from '../../../model/video';
8 | import { DEFAULT_PLAYBACKRATES } from '../../../utils/const';
9 |
10 | export default class PlaybackRateList extends List {
11 | displayName = 'PlaybackRateList';
12 | onSelect = value => {
13 | return this.onSelectEvent(value, () => {
14 | this.dispatch({
15 | type: `${videoNamespace}/playbackRate`,
16 | payload: value,
17 | });
18 | });
19 | };
20 | render() {
21 | const { playbackRate } = this.props;
22 | const { playbackRates = DEFAULT_PLAYBACKRATES } = this.props;
23 | const locale = this.getLocale();
24 | return (
25 |
26 | {this.renderBack(locale.speed)}
27 | {playbackRates &&
28 | playbackRates.map((v, k) => {
29 | const className = classnames({
30 | 'html5-player-rate-selected': playbackRate === v,
31 | });
32 | return (
33 | -
34 | {v + locale.speed}
35 |
36 | );
37 | })}
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/view/controlbar/setting/subtitle-list.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import classnames from 'classnames';
5 | //内部依赖包
6 | import List from './list';
7 | import { namespace as videoNamespace } from '../../../model/video';
8 | import { namespace as trackNamespace } from '../../../model/track';
9 |
10 | @connect(state => {
11 | return {
12 | subtitleList: state[trackNamespace].subtitleList,
13 | subtitleId: state[trackNamespace].subtitleId,
14 | };
15 | })
16 | export default class SubtitleList extends List {
17 | displayName = 'SubtitleList';
18 | onSelect = value => {
19 | return this.onSelectEvent(value, () => {
20 | this.dispatch({
21 | type: `${videoNamespace}/switchSubtitle`,
22 | payload: value,
23 | });
24 | });
25 | };
26 | render() {
27 | const { subtitleList, subtitleId } = this.props;
28 | const locale = this.getLocale();
29 | return (
30 |
31 | {this.renderBack(locale.subtitle)}
32 | {subtitleList &&
33 | subtitleList.map((v, k) => {
34 | const className = classnames({
35 | 'html5-player-list-selected': subtitleId === v.id,
36 | });
37 | return (
38 | -
39 | {v.name}
40 |
41 | );
42 | })}
43 | -
50 | {locale.subtitleOff}
51 |
52 |
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/view/controlbar/subtitle-select.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | // import classnames from 'classnames';
7 | //内部依赖包
8 | import Tooltip from '../components/tooltip';
9 | import clearDecorator from '../decorator/clear';
10 | import SubtitleList from './setting/subtitle-list';
11 | import { namespace as trackNamespace } from '../../model/track';
12 |
13 | @connect(state => {
14 | return {
15 | subtitleList: state[trackNamespace].subtitleList,
16 | subtitleId: state[trackNamespace].subtitleId,
17 | };
18 | })
19 | @clearDecorator([])
20 | export default class SubtitleSelect extends React.Component {
21 | displayName = 'subtitleSelect';
22 | state = {};
23 | onRateSelect = rate => {
24 | this.setState({
25 | tooltipKey: rate,
26 | });
27 | };
28 | renderContent() {
29 | return ;
30 | }
31 | render() {
32 | const { subtitleList, locale } = this.props;
33 | const { tooltipKey } = this.state;
34 | if (!subtitleList[0]) {
35 | return false;
36 | }
37 | return (
38 |
44 |
45 | {locale.subtitle}
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/view/controlbar/time-container.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | //import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from '../decorator/clear';
9 | import { namespace as timeNamespace } from '../../model/time';
10 | import { namespace as fragmentNamespace } from '../../model/fragment';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | return {
17 | time: state[timeNamespace],
18 | fragment: state[fragmentNamespace],
19 | };
20 | })
21 | @clearDecorator([timeNamespace])
22 | export default class Time extends React.Component {
23 | //这里的配置参考jw-player的api
24 | static propTypes = {};
25 | displayName = 'Time';
26 | state = {};
27 | dispatch = this.props.dispatch;
28 | render() {
29 | //fragment是播放录像(合成录像,摄像头上传视频会中断,会分成几个视频,然后这几个视频会合并成一个视频
30 | //但是这个视频不是整个时段的,会有断的,fragment就是给用户知道这段录像哪里断了)
31 | //一般都用不到fragment,如果存在fragment就优先使用fragment的duration
32 | const {
33 | time: { elapse, duration },
34 | fragment,
35 | } = this.props;
36 | return (
37 |
38 | {elapse}
39 | /
40 | {(fragment && fragment.timeDuration) || duration}
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/view/controlbar/time-tooltip.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | import isString from 'lodash/isString';
8 | //内部依赖包
9 | import Tooltip from '../components/tooltip';
10 | import { hms, dateFormat } from '../../utils/util';
11 | import { namespace as timeNamespace } from '../../model/time';
12 | import { namespace as trackerNamespace } from '../../model/track';
13 | import { namespace as fragmentNamespace } from '../../model/fragment';
14 |
15 | @connect(state => {
16 | const thumbnails = state[trackerNamespace].thumbnails;
17 | const props = {
18 | duration: state[timeNamespace].secondDuration,
19 | fragment: state[fragmentNamespace],
20 | thumbnails,
21 | };
22 | return props;
23 | })
24 | export default class TimeTooltip extends React.Component {
25 | //这里的配置参考jw-player的api
26 | static propTypes = {};
27 | displayName = 'TimeTooltip';
28 | state = {
29 | percent: 0.5,
30 | };
31 | dispatch = this.props.dispatch;
32 | onChange = percent => {
33 | this.setState({
34 | percent,
35 | });
36 | };
37 | renderContent() {
38 | //historyTrack是播放录像(合成录像,摄像头上传视频会中断,会分成几个视频,然后这几个视频会合并成一个视频
39 | //但是这个视频不是整个时段的,会有断的,historyTrack就是给用户知道这段录像哪里断了)
40 | //一般都用不到historyTrack,如果存在historyTrack就优先使用historyTrack的duration
41 | let { duration, fragment, thumbnails, timeSliderShowFormat } = this.props;
42 | const { videoBeginDateTime } = fragment;
43 | if (duration === 0) {
44 | return false;
45 | }
46 | if (fragment && fragment.duration) {
47 | duration = fragment.duration;
48 | }
49 | const { percent = 0.5 } = this.state;
50 | if (thumbnails && thumbnails[0]) {
51 | const position = percent * duration;
52 | const style = {};
53 | thumbnails.forEach(v => {
54 | if (position >= v.begin && position < v.end) {
55 | let url = '';
56 | if (!isString(v)) {
57 | //一张图片中有多张缩略图
58 | url = v.thumbnail[1];
59 | style.backgroundImage = 'url("' + url + '")';
60 | style.backgroundPosition =
61 | v.thumbnail[2] * -1 + 'px ' + v.thumbnail[3] * -1 + 'px';
62 | style.width = v.thumbnail[4] + 'px';
63 | style.height = v.thumbnail[5] + 'px';
64 | } else {
65 | //单张图片直接做缩略图
66 | url = v.thumbnail;
67 | }
68 | }
69 | });
70 | return (
71 |
76 |
77 |
78 | {hms(percent * duration)}
79 |
80 |
81 | );
82 | } else {
83 | return (
84 |
85 | {(!videoBeginDateTime || timeSliderShowFormat === 'time') &&
86 | hms(percent * duration)}
87 | {videoBeginDateTime &&
88 | timeSliderShowFormat === 'date' &&
89 | dateFormat(
90 | (videoBeginDateTime + percent * duration) * 1000,
91 | 'YYYY-MM-DD HH:mm:ss'
92 | )}
93 |
94 | );
95 | }
96 | }
97 | render() {
98 | return (
99 |
107 |
108 |
109 | );
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/view/decorator/clear.js:
--------------------------------------------------------------------------------
1 | function clearDecorator(modelNamespaceArr) {
2 | return component => {
3 | component.prototype.temp_componentWillUnmount =
4 | component.prototype.componentWillUnmount;
5 | component.prototype.componentWillUnmount = function() {
6 | if (!this.props.dispatch) {
7 | console.error(new Error('props缺少redux的dispatch'));
8 | return;
9 | }
10 | this.temp_componentWillUnmount && this.temp_componentWillUnmount();
11 | modelNamespaceArr.forEach(v => {
12 | this.props.dispatch({
13 | type: `${v}/clear`,
14 | payload: {},
15 | });
16 | });
17 | };
18 | return component;
19 | };
20 | }
21 | export default clearDecorator;
22 |
--------------------------------------------------------------------------------
/src/view/end.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | import { connect } from 'react-redux';
5 | import classnames from 'classnames';
6 | //内部依赖包
7 | import clearDecorator from './decorator/clear';
8 | import { namespace as endNamespace } from '../model/end';
9 | import { namespace as videoNamespace } from '../model/video';
10 |
11 | /**
12 | * 播放器视频播放结束后的组件
13 | */
14 | @connect(state => {
15 | return {
16 | end: state[endNamespace],
17 | };
18 | })
19 | @clearDecorator([endNamespace])
20 | export default class End extends React.Component {
21 | //这里的配置参考jw-player的api
22 | static propTypes = {};
23 | static displayName = 'End';
24 | state = {};
25 | dispatch = this.props.dispatch;
26 | replay = e => {
27 | this.dispatch({
28 | type: `${videoNamespace}/replay`,
29 | });
30 | };
31 | getClassName(flag) {
32 | return classnames('html5-player-cover-view html5-player-end-view', {
33 | 'html5-player-hide': flag,
34 | });
35 | }
36 | render() {
37 | const { end } = this.props;
38 | if (!end) {
39 | return ;
40 | }
41 | return (
42 | {
45 | e.stopPropagation();
46 | }}
47 | onClick={e => {
48 | e.stopPropagation();
49 | }}
50 | >
51 |
63 |
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/view/error-message.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from './decorator/clear';
9 | import { namespace as videoNamespace } from '../model/video';
10 | import { namespace as errorMessageNamespace } from '../model/error-message';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | return {
17 | errorInfo: state[errorMessageNamespace],
18 | };
19 | })
20 | @clearDecorator([errorMessageNamespace])
21 | export default class ErrorMessage extends React.Component {
22 | //这里的配置参考jw-player的api
23 | static propTypes = {};
24 | displayName = 'ErrorMessage';
25 | state = {};
26 | dispatch = this.props.dispatch;
27 | getClassName(flag) {
28 | return classnames(
29 | 'html5-player-cover-view html5-player-error-message-view',
30 | {
31 | 'html5-player-hide': flag,
32 | }
33 | );
34 | }
35 | reload = e => {
36 | this.dispatch({
37 | type: `${videoNamespace}/reload`,
38 | });
39 | };
40 | render() {
41 | const { message } = this.props.errorInfo;
42 | if (!message) {
43 | //这里不return false 是为了方便单元测试判断。
44 | return ;
45 | }
46 | return (
47 | {
50 | e.stopPropagation();
51 | }}
52 | onClick={e => {
53 | e.stopPropagation();
54 | }}
55 | >
56 |
57 |
65 |
{message}
66 |
67 |
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/view/fragment.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | //import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from './decorator/clear';
9 | import { namespace as fragmentNamespace } from '../model/fragment';
10 | /**
11 | * fragment是video断片处理
12 | * 合成录像,摄像头上传视频会中断,会分成几个视频,然后这几个视频会合并成一个视频
13 | * 但是这个视频不是整个时段的,会有断的,为了给用户知道这段录像哪里断了,需要而外处理
14 | */
15 | @connect(state => {
16 | return {};
17 | })
18 | @clearDecorator([fragmentNamespace])
19 | export default class Fragment extends React.Component {
20 | static propTypes = {
21 | //现在支持传对象进来
22 | url: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
23 | };
24 | displayName = 'Fragment';
25 | dispatch = this.props.dispatch;
26 | getData() {
27 | const { url } = this.props;
28 | this.dispatch({
29 | type: `${fragmentNamespace}/fragmentSaga`,
30 | payload: url,
31 | });
32 | }
33 | componentDidMount() {
34 | this.getData();
35 | }
36 | render() {
37 | return false;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/view/history/time-slider/time-tooltip.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //内部依赖包
4 | import Tooltip from '../../components/tooltip';
5 | import { dateFormat } from '../../../utils/util';
6 |
7 | export default class TimeTooltip extends React.Component {
8 | //这里的配置参考jw-player的api
9 | static propTypes = {};
10 | static displayName = 'TimeTooltip';
11 | state = {
12 | percent: 0.5,
13 | };
14 | dispatch = this.props.dispatch;
15 | onChange = percent => {
16 | this.setState({
17 | percent,
18 | });
19 | };
20 | renderContent() {
21 | let { duration, beginDateTime } = this.props;
22 | const { percent } = this.state;
23 | if (duration === 0) {
24 | return false;
25 | }
26 | const formatTime =
27 | beginDateTime &&
28 | dateFormat(
29 | (beginDateTime + percent * duration) * 1000,
30 | 'YYYY-MM-DD HH:mm:ss'
31 | );
32 | return (
33 | {formatTime}
34 | );
35 | }
36 | render() {
37 | return (
38 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/view/loading.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | import isString from 'lodash/isString';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from './decorator/clear';
9 | import { namespace as loadingNamespace } from '../model/loading';
10 | import { namespace as errorMessageNamespace } from '../model/error-message';
11 |
12 | /**
13 | * 播放器加载状态的组件
14 | */
15 | @connect(state => {
16 | const { loading, message: loadingMessage, retryReloadTime, type } = state[
17 | loadingNamespace
18 | ];
19 | return {
20 | loading,
21 | loadingMessage,
22 | retryReloadTime,
23 | type,
24 | errorInfo: state[errorMessageNamespace],
25 | };
26 | })
27 | @clearDecorator([loadingNamespace])
28 | export default class Loading extends React.Component {
29 | //这里的配置参考jw-player的api
30 | static propTypes = {};
31 | displayName = 'Loading';
32 | state = {};
33 | dispatch = this.props.dispatch;
34 | getClassName(flag) {
35 | const { loadingMessage } = this.props;
36 | return classnames('html5-player-cover-view html5-player-loading-view', {
37 | 'html5-player-hide': flag,
38 | 'html5-player-loading-view-message': loadingMessage,
39 | });
40 | }
41 | renderLoadingMessage() {
42 | const {
43 | loadingMessage,
44 | LoadingMessageComponent,
45 | retryReloadTime,
46 | type,
47 | } = this.props;
48 | let props = {
49 | count: retryReloadTime,
50 | loadingMessage,
51 | type,
52 | };
53 | if (LoadingMessageComponent && isString(LoadingMessageComponent.type)) {
54 | props = {};
55 | }
56 | return (
57 |
58 | {loadingMessage && LoadingMessageComponent
59 | ? React.cloneElement(LoadingMessageComponent, props)
60 | : loadingMessage}
61 |
62 | );
63 | }
64 | render() {
65 | const { loading } = this.props;
66 | const { message: errorMessage } = this.props.errorInfo;
67 | // console.log(loadingMessage);
68 | if (!loading || errorMessage) {
69 | //这里不return false 是为了方便单元测试判断。
70 | return ;
71 | }
72 | return (
73 | {
76 | e.stopPropagation();
77 | }}
78 | onClick={e => {
79 | e.stopPropagation();
80 | }}
81 | >
82 |
88 | {this.renderLoadingMessage()}
89 |
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/view/not-autoplay.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from './decorator/clear';
9 | import { namespace as notAutoPlayNamespace } from '../model/not-autoplay';
10 | import { namespace as videoNamespace } from '../model/video';
11 |
12 | /**
13 | * 播放器视频播放结束后的组件
14 | */
15 | @connect(state => {
16 | return {
17 | show: state[notAutoPlayNamespace],
18 | };
19 | })
20 | @clearDecorator([notAutoPlayNamespace])
21 | export default class NotAutoPlay extends React.Component {
22 | //这里的配置参考jw-player的api
23 | static propTypes = {};
24 | displayName = 'NotAutoPlay';
25 | dispatch = this.props.dispatch;
26 | getClassName(flag) {
27 | return classnames('html5-player-cover-view html5-player-play-view', {
28 | 'html5-player-hide': flag,
29 | });
30 | }
31 | render() {
32 | const { show } = this.props;
33 | if (!show) {
34 | //这里不return false 是为了方便单元测试判断。
35 | return ;
36 | }
37 | return (
38 | {
41 | e.stopPropagation();
42 | }}
43 | onClick={e => {
44 | e.stopPropagation();
45 | }}
46 | >
47 | {show && (
48 |
65 | )}
66 |
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/view/title.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { connect } from 'react-redux';
5 | import classnames from 'classnames';
6 | //内部依赖包
7 | import { namespace as controlbarNamespace } from '../model/controlbar';
8 |
9 | /**
10 | * 播放器加载状态的组件
11 | */
12 | @connect(state => {
13 | return {
14 | userActive: state[controlbarNamespace],
15 | };
16 | })
17 | export default class Title extends React.Component {
18 | static propTypes = {
19 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
20 | };
21 | displayName = 'Title';
22 | state = {};
23 | render() {
24 | const { userActive, title } = this.props;
25 | return (
26 |
31 | {title}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/view/track/subtitle.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | //import ReactDOM from 'react-dom';
4 | //import PropTypes from 'prop-types';
5 | import { connect } from 'react-redux';
6 | import classnames from 'classnames';
7 | //内部依赖包
8 | import clearDecorator from '../decorator/clear';
9 | import { namespace as trackNamespace } from '../../model/track';
10 | import { namespace as timeNamespace } from '../../model/time';
11 | import { namespace as endNamespace } from '../../model/end';
12 | import { namespace as errorMessageNamespace } from '../../model/error-message';
13 |
14 | /**
15 | * 播放器加载状态的组件
16 | */
17 | @connect(state => {
18 | return {
19 | track: state[trackNamespace],
20 | time: state[timeNamespace],
21 | end: state[endNamespace],
22 | isError: !!state[errorMessageNamespace].message,
23 | };
24 | })
25 | @clearDecorator([trackNamespace])
26 | export default class Subtitle extends React.Component {
27 | static propTypes = {};
28 | displayName = 'Subtitle';
29 | render() {
30 | const { track, time, userActive, end, isError } = this.props;
31 | const { subtitleCues } = track;
32 | const currentTime = time.currentTime;
33 | if (!subtitleCues || isError) {
34 | return ;
35 | }
36 | let text;
37 | subtitleCues.forEach(v => {
38 | if (currentTime >= v.begin && currentTime < v.end) {
39 | text = v.text;
40 | }
41 | });
42 | //console.log(text,currentTime)
43 | if (!text || end) {
44 | //text为空或者undefined
45 | return false;
46 | }
47 | //回车键需要处理成
48 | var textArray = text.split('\r\n');
49 | if (textArray.length === 1) {
50 | textArray = text.split('\n');
51 | }
52 | return (
53 |
59 | {textArray.map((v, k) => {
60 | return (
61 |
62 |
63 | {v}
64 |
65 |
66 |
67 | );
68 | })}
69 |
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dog-days/html5-player/94b6e34c39c3ec13f1d3f0166038d1d5618b5ed2/tests/assets/logo.png
--------------------------------------------------------------------------------
/tests/events/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/events/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
32 | document.getElementById(id)
33 | );
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/tests/model/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import outsideApi from '../unit/outside-api';
12 | import * as videoUnit from '../unit/model/video.js';
13 | import { childListChangeObserver } from '../../util';
14 | const unit = [];
15 |
16 | class ModelRegister extends React.Component {
17 | static contextTypes = {
18 | sagaStore: PropTypes.object,
19 | };
20 | displayName = 'ModelRegister';
21 | state = {};
22 | registerModel = register => {
23 | const allModelPromise = modelList.map(modelId => {
24 | const model = require(`src/model/${modelId}.js`).default;
25 | return { model, modelId };
26 | });
27 | const store = this.context.sagaStore;
28 | return Promise.all(allModelPromise)
29 | .then(models => {
30 | models.forEach(m => {
31 | let model = m.model();
32 | switch (m.modelId) {
33 | case 'video/index':
34 | videoUnit.getModelObject(
35 | model,
36 | this.props,
37 | store.dispatch,
38 | store
39 | );
40 | break;
41 | default:
42 | }
43 | register(model);
44 | });
45 | return models;
46 | })
47 | .then(function(models) {
48 | models.forEach(m => {
49 | switch (m.modelId) {
50 | case 'video/index':
51 | unit.push(videoUnit.default);
52 | break;
53 | default:
54 | }
55 | });
56 | });
57 | };
58 | componentDidMount() {
59 | this.registerModel(this.context.sagaStore.register).then(() => {
60 | this.setState({
61 | canBeRendered: true,
62 | });
63 | });
64 | }
65 | render() {
66 | const { children } = this.props;
67 | if (this.state.canBeRendered) {
68 | return {children};
69 | } else {
70 | return false;
71 | }
72 | }
73 | }
74 | export default function player(props) {
75 | return (
76 | {
81 | console.error(error);
82 | },
83 | },
84 | ]}
85 | >
86 |
87 | {
95 | v(player, props.resolve);
96 | });
97 | });
98 | }}
99 | />
100 |
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/tests/model/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Player from '../components/player';
4 |
5 | export default function(id) {
6 | return new Promise(function(resolve) {
7 | ReactDOM.render(
8 | ,
27 | document.getElementById(id)
28 | );
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/tests/model/unit/outside-api.js:
--------------------------------------------------------------------------------
1 | export default function(player) {
2 | describe('External Api', function() {
3 | it(`External Api should be equaled.`, function() {
4 | const api = [
5 | 'on',
6 | 'off',
7 | 'setCurrentTime',
8 | 'play',
9 | 'pause',
10 | 'setVolume',
11 | 'setMuted',
12 | 'replay',
13 | 'setSeeking',
14 | 'fullscreen',
15 | 'controlbar',
16 | 'showErrorMessage',
17 | 'setPlaybackRate',
18 | 'playing',
19 | 'ended',
20 | 'loading',
21 | 'bufferTime',
22 | 'seeking',
23 | 'currentTime',
24 | 'duration',
25 | 'isError',
26 | ];
27 | api.forEach(v => {
28 | expect(!!~Object.keys(player).indexOf(v)).to.equal(true);
29 | });
30 | });
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/tests/playlist/unit/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Html5PlayerList from '../../../src/playlist';
5 | import videoUnit from './model/video.js';
6 | import { childListChangeObserver } from '../../util';
7 |
8 | export default function(id) {
9 | const playlist = [];
10 | const count = 20;
11 | for (var i = 0; i < count; i++) {
12 | let obj = {
13 | title: `第${i + 1}集`,
14 | cover:
15 | 'https://t12.baidu.com/it/u=2991737441,599903151&fm=173&app=25&f=JPEG?w=538&h=397&s=ECAA21D53C330888369488B703006041',
16 | };
17 | //random是为了让file不一样,一样的file切换的时候是不会重新载入的。
18 | obj.file = `https://dog-days.github.io/demo/static/react.mp4?random=${Math.random()}`;
19 | playlist.push(obj);
20 | }
21 | let firstTimeRun = true;
22 | return new Promise(function(resolve) {
23 | ReactDOM.render(
24 | ,
39 | document.getElementById(id)
40 | );
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/tests/playlist/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 |
5 | import { q, mockMouseEvent, attributesChangeObserver } from '../../../util';
6 |
7 | export default function(player, itemCount, resolve) {
8 | describe('Playlist', function() {
9 | const innerContainer = q('.html5-player-carousel-inner-contianer');
10 | it('Playlist count should be correct.', function() {
11 | //eslint-disable-next-line
12 | expect(innerContainer.children.length).to.equal(itemCount);
13 | });
14 | //html5-player-prev-icon
15 | it('Controlbar prev icon should not be rendered when activeItem === 1.', function() {
16 | //activeItem=1时,是第一个,prev按钮不渲染
17 | //eslint-disable-next-line
18 | expect(!!q('.html5-player-prev-icon')).to.be.false;
19 | });
20 | it('Controlbar next icon should be rendered.', function() {
21 | //eslint-disable-next-line
22 | expect(!!q('.html5-player-next-icon')).to.be.true;
23 | });
24 |
25 | it('Playlist active item should be render correctly.', function(done) {
26 | //eslint-disable-next-line
27 | expect(
28 | !!~innerContainer.children[0].classList.value
29 | .split(' ')
30 | .indexOf('html5-player-carousel-item-active')
31 | ).to.be.true;
32 | //eslint-disable-next-line
33 | attributesChangeObserver(innerContainer.children[1], function() {
34 | //eslint-disable-next-line
35 | expect(
36 | !!~innerContainer.children[1].classList.value
37 | .split(' ')
38 | .indexOf('html5-player-carousel-item-active')
39 | ).to.be.true;
40 | done();
41 | });
42 | //点击列表中的第二个视频
43 | mockMouseEvent(innerContainer.children[1], 'click');
44 | });
45 | it('Controlbar prev icon should be rendered when activeItem !== 1.', function() {
46 | //eslint-disable-next-line
47 | expect(!!q('.html5-player-prev-icon')).to.be.true;
48 | });
49 | it('It should work when switching to next video.', function(done) {
50 | //eslint-disable-next-line
51 | attributesChangeObserver(innerContainer.children[2], function() {
52 | //eslint-disable-next-line
53 | expect(
54 | !!~innerContainer.children[2].classList.value
55 | .split(' ')
56 | .indexOf('html5-player-carousel-item-active')
57 | ).to.be.true;
58 | done();
59 | });
60 | mockMouseEvent(q('.html5-player-next-icon').parentElement, 'click');
61 | });
62 | it('It should work when switching to prev video.', function(done) {
63 | //eslint-disable-next-line
64 | attributesChangeObserver(innerContainer.children[1], function() {
65 | //eslint-disable-next-line
66 | expect(
67 | !!~innerContainer.children[1].classList.value
68 | .split(' ')
69 | .indexOf('html5-player-carousel-item-active')
70 | ).to.be.true;
71 | done();
72 | });
73 | mockMouseEvent(q('.html5-player-prev-icon').parentElement, 'click');
74 | });
75 | it('Controlbar next icon should not be rendered when activeItem === 20.', function(done) {
76 | attributesChangeObserver(innerContainer.children[19], function() {
77 | //eslint-disable-next-line
78 | expect(
79 | !!~innerContainer.children[19].classList.value
80 | .split(' ')
81 | .indexOf('html5-player-carousel-item-active')
82 | ).to.be.true;
83 | //eslint-disable-next-line
84 | expect(!!q('.html5-player-next-icon')).to.be.false;
85 | resolve();
86 | done();
87 | });
88 | //点击列表中的第二个视频
89 | mockMouseEvent(innerContainer.children[19], 'click');
90 | });
91 | });
92 | }
93 |
--------------------------------------------------------------------------------
/tests/props-2/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props-2/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
18 | document.getElementById(id)
19 | );
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/tests/props-2/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = ['muted'];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | function itTitle(propsStr, suffix = '.') {
25 | if (suffix !== '.') {
26 | suffix = ',' + suffix;
27 | }
28 | return `props.${propsStr} should work$$`.replace('$$', suffix);
29 | }
30 | export default function(player, resolve) {
31 | describe('Props', function() {
32 | this.timeout(5000);
33 | it(`Play view should be shown when player config autoplay is set to false or undefined.`, function() {
34 | //eslint-disable-next-line
35 | expect(!!_config.autoplay).to.be.false;
36 | const playViewDom = q('.html5-player-play-view');
37 | //eslint-disable-next-line
38 | expect(!!playViewDom.innerHTML).to.be.true;
39 | });
40 | it(itTitle('preload'), function() {
41 | //autoplay=flase才会生效
42 | const playViewDom = q('.html5-player-play-view');
43 | //eslint-disable-next-line
44 | expect(!!playViewDom.innerHTML).to.be.true;
45 | //eslint-disable-next-line
46 | expect(!!q('.html5-player-tag').getAttribute('src')).to.be.false;
47 | });
48 | it(itTitle('muted'), function(done) {
49 | // childListChangeObserver('.html5-player-container', function() {
50 | //初始化会默认自动设置一次声音大小 + 设置为0的一次
51 | expect(spyObj.muted.callCount).to.equal(1);
52 | //要设置isLiving,隐藏timeslider,这样就只有一个.html5-player-slider-track
53 | const sliderTrackDom = q('.html5-player-slider-track');
54 | expect(sliderTrackDom.style.height).to.equal('0%');
55 | //icon展示也要想要改变
56 | let volumeIconDom = q('.html5-player-volume-x-icon');
57 | //eslint-disable-next-line
58 | expect(!!volumeIconDom).to.be.true;
59 | done();
60 | });
61 | it(itTitle('isLiving'), function() {
62 | //eslint-disable-next-line
63 | expect(!!q('.html5-player-time-container')).to.be.false;
64 | //eslint-disable-next-line
65 | expect(!!q('.html5-player-time-slider')).to.be.false;
66 | });
67 | it(itTitle('height'), function() {
68 | //eslint-disable-next-line
69 | expect(q('.html5-player-container').style.height).to.equal('500px');
70 | });
71 | it('props.aspectratio should not work,when props.width is not set.', function(done) {
72 | //width不设置默认是100%,aspectratio将失效。
73 | expect(q('.html5-player-container').clientWidth).to.equal(
74 | document.body.clientWidth
75 | );
76 | setTimeout(function() {
77 | resolve();
78 | done();
79 | }, 100);
80 | });
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-element/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-element/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 |
15 |
16 | demo
17 | ,
18 |
19 | demo2
20 | ,
21 |
22 | }
23 | />,
24 | document.getElementById(id)
25 | );
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-element/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = [];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | export default function(player, resolve) {
25 | describe('Props', function(done) {
26 | it('props.contextMenu should work,when contextMenu is react element.', function(done) {
27 | //eslint-disable-next-line
28 | expect(!!q('.context-menu-test')).to.be.true;
29 | //eslint-disable-next-line
30 | expect(q('.context-menu-test').children.length).to.equal(2);
31 | setTimeout(function() {
32 | resolve();
33 | done();
34 | }, 100);
35 | });
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-false/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-false/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
15 | document.getElementById(id)
16 | );
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/tests/props.contextMenu-false/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = [];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | export default function(player, resolve) {
25 | describe('Props', function(done) {
26 | it('props.contextMenu should work,when contextMenu is false.', function(done) {
27 | //eslint-disable-next-line
28 | expect(!!q('.html5-player-list-container')).to.be.false;
29 | setTimeout(function() {
30 | resolve();
31 | done();
32 | }, 100);
33 | });
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/tests/props.fragment-object/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.fragment-object/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
34 | document.getElementById(id)
35 | );
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/tests/props.fragment-object/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | const spyObj = {};
7 | let _model;
8 | let _dispatch;
9 | let _store;
10 | let _config;
11 | export function getModelObject(model, config, dispatch, store) {
12 | _model = model;
13 | _dispatch = dispatch;
14 | _store = store;
15 | _config = config;
16 | const sagas = [];
17 | sagas.forEach(v => {
18 | spyObj[v] = sinon.spy(model.sagas[v]);
19 | model.sagas[v] = spyObj[v];
20 | });
21 | }
22 | function itTitle(propsStr, suffix = '.') {
23 | if (suffix !== '.') {
24 | suffix = ',' + suffix;
25 | }
26 | return `props.${propsStr} should work$$`.replace('$$', suffix);
27 | }
28 | export default function(player, resolve) {
29 | describe('Props', function(done) {
30 | it(itTitle('fragment', 'when fragment is object.'), function(done) {
31 | setTimeout(function() {
32 | expect(
33 | document.querySelectorAll('.html5-player-broken').length
34 | ).to.equal(3);
35 | resolve();
36 | done();
37 | }, 500);
38 | });
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/tests/props.fragment-string/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.fragment-string/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
25 | document.getElementById(id)
26 | );
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/tests/props.fragment-string/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | const spyObj = {};
7 | let _model;
8 | let _dispatch;
9 | let _store;
10 | let _config;
11 | export function getModelObject(model, config, dispatch, store) {
12 | _model = model;
13 | _dispatch = dispatch;
14 | _store = store;
15 | _config = config;
16 | const sagas = [];
17 | sagas.forEach(v => {
18 | spyObj[v] = sinon.spy(model.sagas[v]);
19 | model.sagas[v] = spyObj[v];
20 | });
21 | }
22 | function itTitle(propsStr, suffix = '.') {
23 | if (suffix !== '.') {
24 | suffix = ',' + suffix;
25 | }
26 | return `props.${propsStr} should work$$`.replace('$$', suffix);
27 | }
28 | export default function(player, resolve) {
29 | describe('Props', function(done) {
30 | this.timeout(5000);
31 | it(itTitle('fragment', 'when fragment is string.'), function(done) {
32 | setTimeout(function() {
33 | expect(
34 | document.querySelectorAll('.html5-player-broken').length
35 | ).to.equal(3);
36 | resolve();
37 | done();
38 | }, 1500);
39 | });
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/tests/props.logo-element/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.logo-element/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 | import logo from 'tests/assets/logo.png';
6 |
7 | export default function(id) {
8 | return new Promise(function(resolve) {
9 | ReactDOM.render(
10 |
19 |
20 |
21 | }
22 | />,
23 | document.getElementById(id)
24 | );
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/tests/props.logo-element/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = [];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | export default function(player, resolve) {
25 | describe('Props', function(done) {
26 | it('props.logo should work,when logo is react element.', function(done) {
27 | //eslint-disable-next-line
28 | expect(!!q('.logo-test')).to.be.true;
29 | setTimeout(function() {
30 | resolve();
31 | done();
32 | }, 100);
33 | });
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/tests/props.logo-string/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.logo-string/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 | import logo from 'tests/assets/logo.png';
6 |
7 | export default function(id) {
8 | return new Promise(function(resolve) {
9 | ReactDOM.render(
10 | ,
16 | document.getElementById(id)
17 | );
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/tests/props.logo-string/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = [];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | export default function(player, resolve) {
25 | describe('Props', function(done) {
26 | it('props.logo should work,when logo is string.', function(done) {
27 | //eslint-disable-next-line
28 | expect(!!q('.html5-player-logo-container').getAttribute('src')).to.be
29 | .true;
30 | setTimeout(function() {
31 | resolve();
32 | done();
33 | }, 100);
34 | });
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/tests/props.tracks/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props.tracks/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 |
6 | export default function(id) {
7 | return new Promise(function(resolve) {
8 | ReactDOM.render(
9 | ,
30 | document.getElementById(id)
31 | );
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/tests/props.tracks/unit/model/video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 可能会受网络的影响,测试也会不通过的,所有需要保证网络没问题。
3 | */
4 | import sinon from 'sinon';
5 |
6 | import { q, childListChangeObserver } from '../../../util';
7 |
8 | const spyObj = {};
9 | let _model;
10 | let _dispatch;
11 | let _store;
12 | let _config;
13 | export function getModelObject(model, config, dispatch, store) {
14 | _model = model;
15 | _dispatch = dispatch;
16 | _store = store;
17 | _config = config;
18 | const sagas = [];
19 | sagas.forEach(v => {
20 | spyObj[v] = sinon.spy(model.sagas[v]);
21 | model.sagas[v] = spyObj[v];
22 | });
23 | }
24 | export default function(player, resolve) {
25 | describe('Props', function() {
26 | this.timeout(5000);
27 | it('props.tracks should work,when kind is "subtitle".', function(done) {
28 | childListChangeObserver('.html5-player-controlbar', function() {
29 | //eslint-disable-next-line
30 | expect(!!q('.html5-player-subtitle-button')).to.be.true;
31 | done();
32 | });
33 | });
34 | it('props.tracks should work,when kind is "thumbnail".', function(done) {
35 | setTimeout(function() {
36 | //eslint-disable-next-line
37 | expect(!!q('.html5-player-thumbnail')).to.be.true;
38 | resolve();
39 | done();
40 | }, 2000);
41 | });
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/tests/props/components/player.jsx:
--------------------------------------------------------------------------------
1 | //外部依赖包
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | //内部依赖包
5 | import Provider from 'src/libs/provider/saga-model-provider';
6 | import modelList from 'src/model-list';
7 | import View from 'src/view';
8 | //icon的js
9 | import 'src/assets/icon/iconfont';
10 |
11 | import * as videoUnit from '../unit/model/video.js';
12 | import { childListChangeObserver } from '../../util';
13 | const unit = [];
14 |
15 | class ModelRegister extends React.Component {
16 | static contextTypes = {
17 | sagaStore: PropTypes.object,
18 | };
19 | displayName = 'ModelRegister';
20 | state = {};
21 | registerModel = register => {
22 | const allModelPromise = modelList.map(modelId => {
23 | const model = require(`src/model/${modelId}.js`).default;
24 | return { model, modelId };
25 | });
26 | const store = this.context.sagaStore;
27 | return Promise.all(allModelPromise)
28 | .then(models => {
29 | models.forEach(m => {
30 | let model = m.model();
31 | switch (m.modelId) {
32 | case 'video/index':
33 | videoUnit.getModelObject(
34 | model,
35 | this.props,
36 | store.dispatch,
37 | store
38 | );
39 | break;
40 | default:
41 | }
42 | register(model);
43 | });
44 | return models;
45 | })
46 | .then(function(models) {
47 | models.forEach(m => {
48 | switch (m.modelId) {
49 | case 'video/index':
50 | unit.push(videoUnit.default);
51 | break;
52 | default:
53 | }
54 | });
55 | });
56 | };
57 | componentDidMount() {
58 | this.registerModel(this.context.sagaStore.register).then(() => {
59 | this.setState({
60 | canBeRendered: true,
61 | });
62 | });
63 | }
64 | render() {
65 | const { children } = this.props;
66 | if (this.state.canBeRendered) {
67 | return {children};
68 | } else {
69 | return false;
70 | }
71 | }
72 | }
73 | export default function player(props) {
74 | return (
75 | {
80 | console.error(error);
81 | },
82 | },
83 | ]}
84 | >
85 |
86 | {
92 | v(player, props.resolve);
93 | });
94 | });
95 | }}
96 | />
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/tests/props/unit/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import Player from '../components/player';
5 | import logo from 'tests/assets/logo.png';
6 |
7 | export default function(id) {
8 | return new Promise(function(resolve) {
9 | ReactDOM.render(
10 |
31 |
34 |
35 | ),
36 | }}
37 | logo={{
38 | image: logo,
39 | link: 'https://github.com/dog-days/html5-player',
40 | }}
41 | poster={logo}
42 | playbackRates={[0.5, 1]}
43 | contextMenu={[demo, demo2]}
44 | />,
45 | document.getElementById(id)
46 | );
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/tests/start.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import * as storage from 'src/utils/storage';
5 | import { childListChangeObserver } from './util';
6 | import model from './model/unit';
7 | import props from './props/unit';
8 | import props2 from './props-2/unit';
9 | import propsTracks from './props.tracks/unit';
10 | import propsFragmentString from './props.fragment-string/unit';
11 | import propsFragmentObject from './props.fragment-object/unit';
12 | import propsContextMenuFalse from './props.contextMenu-false/unit';
13 | import propsContextMenuElement from './props.contextMenu-element/unit';
14 | import propsLogoString from './props.logo-string/unit';
15 | import propsLogoElement from './props.logo-element/unit';
16 | import propsPlaylist from './playlist/unit';
17 | import events from './events/unit';
18 |
19 | const id = 'test';
20 | const div = document.createElement('div');
21 | div.setAttribute('id', id);
22 | document.body.appendChild(div);
23 |
24 | model(id)
25 | .then(function({ lastModel, lastDispatch, lastSpyObj }) {
26 | return new Promise(function(resolve, reject) {
27 | describe('Model', function() {
28 | it(`model/video/index "clear" reducer should be executed correctly.`, function(done) {
29 | //删除播放器
30 | childListChangeObserver('#' + id, function() {
31 | //reload saga已经触发了一次clear。
32 | expect(lastSpyObj.clear.callCount).to.equal(2);
33 | resolve();
34 | done();
35 | });
36 | ReactDOM.render(, document.getElementById(id));
37 | });
38 | });
39 | });
40 | })
41 | .then(function() {
42 | return props(id);
43 | })
44 | .then(function() {
45 | return props2(id);
46 | })
47 | .then(function() {
48 | return propsTracks(id);
49 | })
50 | .then(function() {
51 | return propsFragmentString(id);
52 | })
53 | .then(function() {
54 | return propsFragmentObject(id);
55 | })
56 | .then(function() {
57 | return propsContextMenuFalse(id);
58 | })
59 | .then(function() {
60 | return propsContextMenuElement(id);
61 | })
62 | .then(function() {
63 | return propsLogoString(id);
64 | })
65 | .then(function() {
66 | return propsLogoElement(id);
67 | })
68 | .then(function() {
69 | return propsPlaylist(id);
70 | })
71 | .then(function() {
72 | storage.set('muted', false);
73 | storage.set('volume', 20);
74 | return events(id);
75 | });
76 |
--------------------------------------------------------------------------------
/tests/util.js:
--------------------------------------------------------------------------------
1 | export function q(query) {
2 | if (Object.prototype.toString.apply(query) === '[object String]') {
3 | return document.querySelector(query);
4 | } else {
5 | return query;
6 | }
7 | }
8 | // Firefox和Chrome早期版本中带有前缀
9 | const MutationObserver =
10 | window.MutationObserver ||
11 | window.WebKitMutationObserver ||
12 | window.MozMutationObserver;
13 | let observer;
14 | export function childListChangeObserver(selector, done) {
15 | if (observer) {
16 | observer.disconnect();
17 | }
18 | // 选择目标节点
19 | let target = q(selector);
20 | // 创建观察者对象
21 | observer = new MutationObserver(function(mutations) {
22 | // 停止观察
23 | observer.disconnect();
24 | observer = undefined;
25 | done();
26 | });
27 | // 配置观察选项:
28 | let config = { childList: true, subtree: true };
29 | // 传入目标节点和观察选项
30 | observer.observe(target, config);
31 | }
32 | export function shouldChildNotEmptyObserver(selector, done) {
33 | if (observer) {
34 | observer.disconnect();
35 | }
36 | // 选择目标节点
37 | let target = q(selector);
38 | // 创建观察者对象
39 | observer = new MutationObserver(function(mutations) {
40 | // 停止观察
41 | observer.disconnect();
42 | observer = undefined;
43 | expect(!!q(selector).innerHTML).to.equal(true);
44 | done();
45 | });
46 | // 配置观察选项:
47 | let config = { childList: true };
48 | // 传入目标节点和观察选项
49 | observer.observe(target, config);
50 | }
51 | export function shouldChildEmptyObserver(selector, done) {
52 | if (observer) {
53 | observer.disconnect();
54 | }
55 | // 选择目标节点
56 | let target = q(selector);
57 | // 创建观察者对象
58 | observer = new MutationObserver(function(mutations) {
59 | // 停止观察
60 | observer.disconnect();
61 | observer = undefined;
62 | expect(!q(selector).innerHTML).to.equal(true);
63 | done();
64 | });
65 | // 配置观察选项:
66 | let config = { childList: true };
67 | // 传入目标节点和观察选项
68 | observer.observe(target, config);
69 | }
70 |
71 | export function attributesChangeObserver(selector, callback) {
72 | if (observer) {
73 | observer.disconnect();
74 | }
75 | // 选择目标节点
76 | let target = q(selector);
77 | // 创建观察者对象
78 | observer = new MutationObserver(function(mutations) {
79 | observer.disconnect();
80 | observer = undefined;
81 | callback && callback();
82 | });
83 | // 配置观察选项:
84 | let config = {
85 | attributes: true,
86 | };
87 | // 传入目标节点和观察选项
88 | observer.observe(target, config);
89 | }
90 |
91 | export function isFullscreen() {
92 | const documentContext = document;
93 | return (
94 | documentContext.fullscreenElement ||
95 | documentContext.webkitCurrentFullScreenElement ||
96 | documentContext.mozFullScreenElement ||
97 | documentContext.msFullscreenElement
98 | );
99 | }
100 |
101 | export function mockMouseEvent(dom, type) {
102 | var event = document.createEvent('MouseEvents');
103 | //初始化event
104 | event.initMouseEvent(
105 | type,
106 | true,
107 | true,
108 | document.defaultView,
109 | 0,
110 | 0,
111 | 0,
112 | 0,
113 | 0,
114 | false,
115 | false,
116 | false,
117 | false,
118 | 0,
119 | null
120 | );
121 | dom.dispatchEvent(event);
122 | }
123 |
--------------------------------------------------------------------------------