├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 |
    46 | 源码位置: 47 | 48 | {codeUrl} 49 | 50 |
    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 |
    9 |
    10 | 25 |
    26 |
    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 |
    9 |
    10 | 20 |
    21 |
    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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | --------------------------------------------------------------------------------