├── .DS_Store ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── dashjs.html ├── hlsjs-all-browsers.html ├── hlsjs-sw.html ├── hlsjs.html ├── mp4.html ├── php.html ├── quick-start.html ├── shaka-player.html └── sw.js ├── docs ├── .vuepress │ ├── components │ │ └── DPlayer.vue │ ├── config.js │ ├── public │ │ ├── CNAME │ │ └── logo.png │ └── styles │ │ ├── index.styl │ │ └── palette.styl ├── README.md ├── ecosystem.md ├── guide.md ├── support.md └── zh │ ├── README.md │ ├── ecosystem.md │ ├── guide.md │ └── support.md ├── package.json ├── src ├── assets │ ├── airplay.svg │ ├── camera.svg │ ├── comment-off.svg │ ├── comment.svg │ ├── full-web.svg │ ├── full.svg │ ├── loading.svg │ ├── pallette.svg │ ├── pause.svg │ ├── play.svg │ ├── right.svg │ ├── send.svg │ ├── setting.svg │ ├── subtitle.svg │ ├── volume-down.svg │ ├── volume-off.svg │ └── volume-up.svg ├── css │ ├── balloon.scss │ ├── bezel.scss │ ├── controller.scss │ ├── danmaku.scss │ ├── global.scss │ ├── index.scss │ ├── info-panel.scss │ ├── logo.scss │ ├── menu.scss │ ├── notice.scss │ ├── player.scss │ ├── subtitle.scss │ └── video.scss ├── js │ ├── api.js │ ├── bar.js │ ├── bezel.js │ ├── comment.js │ ├── contextmenu.js │ ├── controller.js │ ├── danmaku.js │ ├── events.js │ ├── fullscreen.js │ ├── hotkey.js │ ├── i18n.js │ ├── icons.js │ ├── index.js │ ├── info-panel.js │ ├── options.js │ ├── play-state.js │ ├── player.js │ ├── setting.js │ ├── subtitle.js │ ├── template.js │ ├── thumbnails.js │ ├── timer.js │ ├── user.js │ └── utils.js └── template │ ├── player.art │ └── video.art ├── webpack ├── dev.config.js └── prod.config.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdnbye/CBPlayer/cf7cfdfbb74da8f2599efea896bdbfb3462ad840/.DS_Store -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "es6": true, 10 | "browser": true, 11 | "node": true, 12 | }, 13 | "rules": { 14 | "block-scoped-var": 1, 15 | "curly": 1, 16 | "eqeqeq": 1, 17 | "no-global-assign": 1, 18 | "no-implicit-globals": 1, 19 | "no-labels": 1, 20 | "no-multi-str": 1, 21 | "comma-spacing": 1, 22 | "comma-style": 1, 23 | "func-call-spacing": 1, 24 | "keyword-spacing": 1, 25 | "linebreak-style": 1, 26 | "no-multiple-empty-lines": 1, 27 | "space-infix-ops": 1, 28 | "arrow-spacing": 1, 29 | "no-var": 1, 30 | "prefer-const": 1, 31 | "no-unsafe-negation": 1, 32 | "array-callback-return": 1, 33 | "dot-notation": 1, 34 | "no-eval": 1, 35 | "no-extend-native": 1, 36 | "no-extra-label": 1, 37 | "semi": 1, 38 | "space-before-blocks": 1, 39 | "space-in-parens": 1, 40 | "space-unary-ops": 1, 41 | "spaced-comment": 1, 42 | "arrow-body-style": 1, 43 | "arrow-parens": 1, 44 | "no-restricted-imports": 1, 45 | "no-duplicate-imports": 1, 46 | "no-useless-computed-key": 1, 47 | "no-useless-rename": 1, 48 | "rest-spread-spacing": 1, 49 | "no-trailing-spaces": 1, 50 | "no-control-regex": 0, 51 | "prettier/prettier": 0, 52 | "no-await-in-loop": 1, 53 | "require-atomic-updates": 0, 54 | "no-prototype-builtins": 0 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: DIYgod 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 中文用户请注意:请尽量用**英文**描述你的 issue,这样能够让尽可能多的人帮到你。 2 | 3 | If you want to report a bug, please provide the following information: 4 | 5 | - The steps to reproduce. 6 | - A minimal demo of the problem via https://jsfiddle.net or http://codepen.io/pen if possible. 7 | - Which versions of DPlayer, and which browser / OS are affected by this issue? 8 | 9 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | demo2 4 | docs2 5 | npm-debug.log 6 | DPlayer.log* 7 | wxw 8 | .vscode 9 | package-lock.json 10 | docs/.vuepress/dist 11 | demo/test.html 12 | demo/mp4-p2p-engine.js 13 | demo/hls.js 14 | demo/hlsjs-p2p-engine.js 15 | demo/shaka-p2p-engine.js 16 | demo/dashjs-p2p-engine.js 17 | demo/test-dash.html 18 | demo/test-hls.html 19 | demo/test-hls-switch.html 20 | demo/test-mp4.html 21 | demo/test-shaka.html 22 | demo/test-dashjs.html 23 | demo/test-all.html 24 | demo/test-hlsde.html 25 | dist 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 233, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | script: 5 | - npm run build 6 | - npm run docs:build 7 | deploy: 8 | provider: pages 9 | skip-cleanup: true 10 | local_dir: docs/.vuepress/dist 11 | github-token: $GITHUB_TOKEN # a token generated on github allowing travis to push code on you repository 12 | keep-history: true 13 | on: 14 | branch: master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) DIYgod (https://www.anotherhome.net/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CBPlayer 3 |

4 |

CBPlayer2

5 | 6 | > 🍭 Wow, such a lovely HTML5 danmaku video player 7 | 8 | [![npm](https://img.shields.io/npm/v/cbplayer2.svg?style=flat-square)](https://www.npmjs.com/package/cbplayer2) 9 | [![npm](https://img.shields.io/npm/l/cbplayer2.svg?style=flat-square)](https://github.com/MoePlayer/DPlayer/blob/master/LICENSE) 10 | [![npm](https://img.shields.io/npm/dt/cbplayer2.svg?style=flat-square)](https://www.npmjs.com/package/cbplayer2) 11 | [![npm](https://data.jsdelivr.com/v1/package/npm/cbplayer2/badge)](https://www.jsdelivr.com/package/npm/cbplayer2) 12 | 13 | ## Introduction 14 | 15 | CBPlayer 是基于 DPlayer 开发的,内置 CDNBye P2P 插件的 H5 播放器,加入了记忆播放等实用功能,右键可以查看p2p实时数据。支持HLS、MP4和MPEG-DASH三种格式的P2P加速。 16 | 17 |
18 | CBPlayer的API与DPlayer保持一致,可以参考DPLayer的官方文档: 19 | 20 | **[Docs](http://dplayer.js.org)** 21 | 22 | **[中文文档](http://dplayer.js.org/#/zh-Hans/)** 23 | 24 | ## 集成方法(Usage) 25 | 26 | ```html 27 | 28 | 33 |
34 | 35 | 36 | 37 | 58 | ``` 59 | 60 | ## 后台管理系统 61 | 在接入P2P插件后,访问`https://www.cdnbye.com/oms`,注册并绑定域名,即可查看该域名的P2P流量、在线人数、用户地理分布等信息。 62 | 63 | ## Console 64 | Register your domain in `https://oms.cdnbye.com`, where you can view p2p-related information. 65 | -------------------------------------------------------------------------------- /demo/dashjs.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 39 | -------------------------------------------------------------------------------- /demo/hlsjs-all-browsers.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 43 | -------------------------------------------------------------------------------- /demo/hlsjs-sw.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /demo/hlsjs.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 40 | -------------------------------------------------------------------------------- /demo/mp4.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 38 | -------------------------------------------------------------------------------- /demo/php.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | dplayer增加记忆+P2P播放 4 | 5 | 6 | 7 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/quick-start.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 56 | -------------------------------------------------------------------------------- /demo/shaka-player.html: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 38 | -------------------------------------------------------------------------------- /demo/sw.js: -------------------------------------------------------------------------------- 1 | self.importScripts('https://cdn.jsdelivr.net/npm/swarmcloud-hls-sw@latest/dist/hls-proxy.js') 2 | 3 | console.info(`HlsProxy version ${HlsProxy.version}`); 4 | 5 | -------------------------------------------------------------------------------- /docs/.vuepress/components/DPlayer.vue: -------------------------------------------------------------------------------- 1 | 6 | 87 | 89 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@vuepress/google-analytics': { 4 | ga: 'UA-48084758-9', 5 | }, 6 | '@vuepress/pwa': { 7 | serviceWorker: true, 8 | updatePopup: { 9 | '/zh/': { 10 | message: '发现新内容可用', 11 | buttonText: '刷新', 12 | }, 13 | '/': { 14 | message: 'New content is available', 15 | buttonText: 'Refresh', 16 | }, 17 | }, 18 | }, 19 | '@vuepress/back-to-top': true, 20 | }, 21 | locales: { 22 | '/zh/': { 23 | lang: 'zh-CN', 24 | title: 'DPlayer', 25 | description: '🍭 Wow, such a lovely HTML5 danmaku video player', 26 | }, 27 | '/': { 28 | lang: 'en-US', 29 | title: 'DPlayer', 30 | description: '🍭 Wow, such a lovely HTML5 danmaku video player', 31 | }, 32 | }, 33 | head: [ 34 | ['link', { rel: 'icon', href: `/logo.png` }], 35 | ['script', { src: 'https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js' }], 36 | ['script', { src: 'https://cdn.jsdelivr.net/npm/hls.js/dist/hls.min.js' }], 37 | ['script', { src: 'https://cdn.jsdelivr.net/npm/dashjs/dist/dash.all.min.js' }], 38 | ['script', { src: 'https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js' }], 39 | ['script', { src: 'https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js' }], 40 | ], 41 | themeConfig: { 42 | repo: 'MoePlayer/DPlayer', 43 | editLinks: true, 44 | docsDir: 'docs', 45 | locales: { 46 | '/zh/': { 47 | lang: 'zh-CN', 48 | selectText: '选择语言', 49 | label: '简体中文', 50 | editLinkText: '在 GitHub 上编辑此页', 51 | lastUpdated: '上次更新', 52 | nav: [ 53 | { 54 | text: '指南', 55 | link: '/zh/guide/', 56 | }, 57 | { 58 | text: '生态', 59 | link: '/zh/ecosystem/', 60 | }, 61 | { 62 | text: '支持 DPlayer', 63 | link: '/zh/support/', 64 | }, 65 | ], 66 | }, 67 | '/': { 68 | lang: 'en-US', 69 | selectText: 'Languages', 70 | label: 'English', 71 | editLinkText: 'Edit this page on GitHub', 72 | lastUpdated: 'Last Updated', 73 | nav: [ 74 | { 75 | text: 'Guide', 76 | link: '/guide/', 77 | }, 78 | { 79 | text: 'Ecosystem', 80 | link: '/ecosystem/', 81 | }, 82 | { 83 | text: 'Support DPlayer', 84 | link: '/support/', 85 | }, 86 | ], 87 | }, 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /docs/.vuepress/public/CNAME: -------------------------------------------------------------------------------- 1 | dplayer.js.org -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdnbye/CBPlayer/cf7cfdfbb74da8f2599efea896bdbfb3462ad840/docs/.vuepress/public/logo.png -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | .navbar .home-link .site-name { 2 | color: #F5712C; 3 | } 4 | 5 | .page .custom-block.tip { 6 | border-color: #F5712C; 7 | } 8 | 9 | .icon.outbound { 10 | display: none; 11 | } 12 | 13 | a { 14 | word-break: break-all; 15 | } 16 | 17 | #指南 { 18 | display: none; 19 | } 20 | 21 | #guide { 22 | display: none; 23 | } 24 | 25 | #app .global-ui .sw-update-popup { 26 | border: 1px solid #F5712C; 27 | } 28 | 29 | .routes .sidebar-group-items > li > .sidebar-sub-headers > .sidebar-sub-header > a { 30 | color: $accentColor; 31 | } 32 | 33 | #dplayer { 34 | margin-top: -1.5rem; 35 | margin-bottom: 1rem; 36 | } 37 | 38 | .hero .description { 39 | display: none; 40 | } 41 | 42 | .hero .action { 43 | display: none; 44 | } 45 | 46 | .hero.custom .action { 47 | display: block; 48 | } -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $accentColor = #F5712C 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: Get Started → 4 | actionLink: /guide/ 5 | footer: MIT Licensed | Made with love by DIYgod 6 | --- 7 | 8 |
9 | 10 |
11 | 12 |

Get Started →

13 | -------------------------------------------------------------------------------- /docs/ecosystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # Ecosystem 6 | 7 | Let's make DPlayer better, feel free to submit yours in [`Let me know!`](https://github.com/MoePlayer/DPlayer/issues/31) 8 | 9 | ## Help 10 | 11 | ### Joining the Discussion 12 | 13 | - [Telegram Group](https://t.me/adplayer) 14 | 15 | ### Creating issue 16 | 17 | - [MoePlayer/DPlayer/issues](https://github.com/MoePlayer/DPlayer/issues) 18 | 19 | ## Related Projects 20 | 21 | ### Tooling 22 | 23 | - [DPlayer-thumbnails](https://github.com/MoePlayer/DPlayer-thumbnails): generate video thumbnails 24 | 25 | ### Danmaku api 26 | 27 | - [DPlayer-node](https://github.com/MoePlayer/DPlayer-node): Node.js 28 | - [laravel-danmaku](https://github.com/MoePlayer/laravel-danmaku): PHP 29 | - [dplayer-live-backend](https://github.com/Izumi-kun/dplayer-live-backend): Node.js, WebSocket live backend 30 | - [RailsGun](https://github.com/MoePlayer/RailsGun): Ruby 31 | 32 | ### Plugins 33 | 34 | - [DPlayer-for-typecho](https://github.com/volio/DPlayer-for-typecho): Typecho 35 | - [Hexo-tag-dplayer](https://github.com/NextMoe/hexo-tag-dplayer): Hexo 36 | - [DPlayer_for_Z-BlogPHP](https://github.com/fghrsh/DPlayer_for_Z-BlogPHP): Z-BlogPHP 37 | - [DPlayer for Discuz!](https://coding.net/u/Click_04/p/video/git): Discuz! 38 | - [DPlayer for WordPress](https://github.com/BlueCocoa/DPlayer-WordPress): WordPress 39 | - [DPlayerHandle](https://github.com/kn007/DPlayerHandle): WordPress 40 | - [Selection](https://github.com/GreatSatan79/Selection): WordPress 41 | - [Vue-DPlayer](https://github.com/sinchang/vue-dplayer): Vue 42 | - [react-dplayer](https://github.com/hnsylitao/react-dplayer): React 43 | 44 | ### Other 45 | 46 | - [DPlayer-Lite](https://github.com/kn007/DPlayer-Lite): lite version 47 | - [hlsjs-p2p-engine](https://github.com/cdnbye/hlsjs-p2p-engine) 48 | 49 | ## Who use DPlayer? 50 | 51 | - [学习强国](https://itunes.apple.com/cn/app/%E5%AD%A6%E4%B9%A0%E5%BC%BA%E5%9B%BD/id1426355645?mt=8): “学习强国”学习平台精心打造的手机客户端 52 | - [小红书](https://www.xiaohongshu.com/): 中国最大的生活社区分享平台,同时也是发现全球好物的电商平台 53 | - [极客时间](https://time.geekbang.org/): 极客邦科技出品的一款 IT 内容知识服务 App 54 | - [嘀哩嘀哩](http://www.dilidili.wang/): 兴趣使然的无名小站(D 站) 55 | - [银色子弹](https://www.sbsub.com/): 银色子弹,简称银弹,由多数柯南热爱者聚集在一起的组织 56 | - [浙江大学 CC98 论坛](https://zh.wikipedia.org/wiki/CC98%E8%AE%BA%E5%9D%9B): 浙江大学校网内规模最大的论坛,中国各大学中较活跃的 BBS 之一 57 | - [纸飞机南航青年网络社区](http://my.nuaa.edu.cn/video-video.html): 南京航空航天大学门户网站 58 | - [otomads](https://otomads.com/): 专注于音 MAD 的视频弹幕网站 59 | - [Cloudreve](https://github.com/HFO4/Cloudreve): 基于 ThinkPHP 构建的网盘系统 60 | 61 | ## Contributors 62 | 63 | This project exists thanks to all the people who contribute. 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # Sponsor DPlayer Development 6 | 7 | DPlayer is an MIT licensed open source project and completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. 8 | 9 | If you run a business and are using DPlayer in a revenue-generating product, it makes business sense to sponsor DPlayer development: it ensures the project that your product relies on stays healthy and actively maintained. 10 | 11 | If you are an individual user and have enjoyed the productivity of using DPlayer, consider donating as a sign of appreciation - like buying me coffee once in a while :) 12 | 13 | You can support DPlayer development via the following methods: 14 | 15 | ## One-time Donations 16 | 17 | We accept donations through these channels: 18 | 19 | - [Paypal](https://www.paypal.me/DIYgod) 20 | - [WeChat Pay](https://i.imgur.com/aq6PtWa.png) 21 | - [Alipay](https://i.imgur.com/wv1Pj2k.png) 22 | 23 | ## Recurring Pledges 24 | 25 | Recurring pledges come with exclusive perks, e.g. having your name or your company logo listed in the DPlayer GitHub repository and this website. 26 | 27 | - Become a Backer or a Sponser on [Patreon](https://www.patreon.com/DIYgod) 28 | - E-mail us: i#diygod.me 29 | 30 | ## Current Premium Sponsors 31 | 32 | ### Special Sponsors 33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 |
45 | 46 | ### Sponsors 47 | 48 | | [极酷社](https://www.acg.app) | 49 | | :---------------------------: | 50 | 51 | ## DPlayer contributors 52 | 53 | This project exists thanks to all the people who contribute. 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: Get Started → 4 | actionLink: /guide/ 5 | footer: MIT Licensed | Made with love by DIYgod 6 | --- 7 | 8 |
9 | 10 |
11 | 12 |

快速上手 →

13 | -------------------------------------------------------------------------------- /docs/zh/ecosystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # 生态 6 | 7 | 让 DPlayer 变得更好,请随意在 [`Let me know!`](https://github.com/MoePlayer/DPlayer/issues/31) 提交你的项目和产品 8 | 9 | ## 帮助 10 | 11 | ### 参与讨论 12 | 13 | - [Telegram 群](https://t.me/adplayer) 14 | 15 | ### 提交 issue 16 | 17 | - [MoePlayer/DPlayer/issues](https://github.com/MoePlayer/DPlayer/issues) 18 | 19 | ## 相关项目 20 | 21 | ### 工具 22 | 23 | - [DPlayer-thumbnails](https://github.com/MoePlayer/DPlayer-thumbnails): generate video thumbnails 24 | 25 | ### 弹幕接口 26 | 27 | - [DPlayer-node](https://github.com/MoePlayer/DPlayer-node): Node.js 28 | - [laravel-danmaku](https://github.com/MoePlayer/laravel-danmaku): PHP 29 | - [dplayer-live-backend](https://github.com/Izumi-kun/dplayer-live-backend): Node.js, WebSocket live backend 30 | - [RailsGun](https://github.com/MoePlayer/RailsGun): Ruby 31 | 32 | ### 插件 33 | 34 | - [DPlayer-for-typecho](https://github.com/volio/DPlayer-for-typecho): Typecho 35 | - [Hexo-tag-dplayer](https://github.com/NextMoe/hexo-tag-dplayer): Hexo 36 | - [DPlayer_for_Z-BlogPHP](https://github.com/fghrsh/DPlayer_for_Z-BlogPHP): Z-BlogPHP 37 | - [DPlayer for Discuz!](https://coding.net/u/Click_04/p/video/git): Discuz! 38 | - [DPlayer for WordPress](https://github.com/BlueCocoa/DPlayer-WordPress): WordPress 39 | - [DPlayerHandle](https://github.com/kn007/DPlayerHandle): WordPress 40 | - [Selection](https://github.com/GreatSatan79/Selection): WordPress 41 | - [Vue-DPlayer](https://github.com/sinchang/vue-dplayer): Vue 42 | - [react-dplayer](https://github.com/hnsylitao/react-dplayer): React 43 | 44 | ### 其他 45 | 46 | - [DPlayer-Lite](https://github.com/kn007/DPlayer-Lite): lite version 47 | - [hlsjs-p2p-engine](https://github.com/cdnbye/hlsjs-p2p-engine) 48 | 49 | ## 谁在用 DPlayer? 50 | 51 | - [学习强国](https://itunes.apple.com/cn/app/%E5%AD%A6%E4%B9%A0%E5%BC%BA%E5%9B%BD/id1426355645?mt=8): “学习强国”学习平台精心打造的手机客户端 52 | - [小红书](https://www.xiaohongshu.com/): 中国最大的生活社区分享平台,同时也是发现全球好物的电商平台 53 | - [极客时间](https://time.geekbang.org/): 极客邦科技出品的一款 IT 内容知识服务 App 54 | - [嘀哩嘀哩](http://www.dilidili.wang/): 兴趣使然的无名小站(D 站) 55 | - [银色子弹](https://www.sbsub.com/): 银色子弹,简称银弹,由多数柯南热爱者聚集在一起的组织 56 | - [浙江大学 CC98 论坛](https://zh.wikipedia.org/wiki/CC98%E8%AE%BA%E5%9D%9B): 浙江大学校网内规模最大的论坛,中国各大学中较活跃的 BBS 之一 57 | - [纸飞机南航青年网络社区](http://my.nuaa.edu.cn/video-video.html): 南京航空航天大学门户网站 58 | - [otomads](https://otomads.com/): 专注于音 MAD 的视频弹幕网站 59 | - [Cloudreve](https://github.com/HFO4/Cloudreve): 基于 ThinkPHP 构建的网盘系统 60 | -------------------------------------------------------------------------------- /docs/zh/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # 赞助 DPlayer 的研发 6 | 7 | DPlayer 是采用 MIT 许可的开源项目,使用完全免费。 但是随着项目规模的增长,也需要有相应的资金支持才能持续项目的维护的开发。 8 | 9 | 如果你是企业经营者并且将 DPlayer 用在商业产品中,那么赞助 DPlayer 有商业上的益处:可以让你的产品所依赖的框架保持健康并得到积极的维护。 10 | 11 | 如果你是个人开发者并且享受 DPlayer 带来的高开发效率,可以用捐助来表示你的谢意 —— 比如偶尔给我买杯咖啡 :) 12 | 13 | 你可以通过下列的方法来赞助 DPlayer 的开发。 14 | 15 | ## 一次性赞助 16 | 17 | 我们通过以下方式接受赞助: 18 | 19 | - [微信支付](https://i.imgur.com/aq6PtWa.png) 20 | - [支付宝](https://i.imgur.com/wv1Pj2k.png) 21 | - [Paypal](https://www.paypal.me/DIYgod) 22 | 23 | ## 周期性赞助 24 | 25 | 周期性赞助可以获得额外的回报,比如你的名字或你的公司 logo 会出现在 DPlayer 的 GitHub 仓库和现在我们的官网中。 26 | 27 | - 通过 [Patreon](https://www.patreon.com/DIYgod) 赞助 28 | - 给我们发邮件联系赞助事宜: i#diygod.me 29 | 30 | ## 当前的顶级赞助商 31 | 32 | ### Special Sponsors 33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 |
45 | 46 | ### Sponsors 47 | 48 | | [极酷社](https://www.acg.app) | 49 | | :---------------------------: | 50 | 51 | ## DPlayer 贡献者 52 | 53 | 感谢所有贡献者。 54 | 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cbplayer2", 3 | "version": "0.12.0", 4 | "description": "CDNBye official web player v2", 5 | "main": "dist/CBPlayer.min.js", 6 | "scripts": { 7 | "start": "npm run dev", 8 | "build": "cross-env NODE_ENV=production webpack --config webpack/prod.config.js --progress", 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.config.js --watch --colors", 10 | "test": "eslint src webpack", 11 | "format": "eslint \"**/*.js\" --fix && prettier \"**/*.{js,json,md}\" --write", 12 | "format:staged": "eslint \"**/*.js\" --fix && pretty-quick --staged --verbose --pattern \"**/*.{js,json,md}\"", 13 | "format:check": "eslint \"**/*.js\" && prettier-check \"**/*.{js,json,md}\"", 14 | "docs:dev": "vuepress dev docs", 15 | "docs:build": "vuepress build docs", 16 | "publish": "npm publish", 17 | "git-push": "git add demo && git add package.json && git add README.md && git add src && git commit -m 'release v0.12.0' && git push" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "repository": { 23 | "url": "git+https://github.com/cdnbye/CBPlayer.git", 24 | "type": "git" 25 | }, 26 | "keywords": [ 27 | "p2p", 28 | "webrtc", 29 | "cdnbye", 30 | "m3u8", 31 | "hls", 32 | "player", 33 | "danmaku", 34 | "video", 35 | "html5" 36 | ], 37 | "gitHooks": { 38 | }, 39 | "author": "cdnbye", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/cdnbye/CBPlayer/issues" 43 | }, 44 | "homepage": "https://www.cdnbye.com", 45 | "devDependencies": { 46 | "@babel/core": "^7.6.0", 47 | "@babel/preset-env": "^7.4.5", 48 | "@vuepress/plugin-back-to-top": "1.7.1", 49 | "@vuepress/plugin-google-analytics": "1.7.1", 50 | "@vuepress/plugin-pwa": "1.7.1", 51 | "art-template": "4.13.2", 52 | "art-template-loader": "1.4.3", 53 | "autoprefixer": "^9.6.1", 54 | "babel-loader": "^8.0.6", 55 | "cross-env": "^7.0.0", 56 | "css-loader": "^5.0.0", 57 | "cssnano": "^4.1.10", 58 | "eslint": "^7.0.0", 59 | "eslint-config-prettier": "^6.3.0", 60 | "eslint-loader": "^4.0.0", 61 | "eslint-plugin-prettier": "^3.1.1", 62 | "exports-loader": "^1.0.0", 63 | "file-loader": "^6.0.0", 64 | "git-revision-webpack-plugin": "^3.0.3", 65 | "mini-css-extract-plugin": "1.3.0", 66 | "node-sass": "^5.0.0", 67 | "postcss-loader": "^3.0.0", 68 | "prettier": "^2.0.4", 69 | "prettier-check": "^2.0.0", 70 | "pretty-quick": "^3.0.0", 71 | "sass-loader": "^10.0.0", 72 | "strip-loader": "^0.1.2", 73 | "style-loader": "^2.0.0", 74 | "svg-inline-loader": "0.8.2", 75 | "template-string-optimize-loader": "^3.0.0", 76 | "url-loader": "^4.1.0", 77 | "vuepress": "1.7.1", 78 | "webpack": "^4.40.2", 79 | "webpack-cli": "3.3.12", 80 | "webpack-dev-server": "^3.8.1", 81 | "yorkie": "^2.0.0" 82 | }, 83 | "dependencies": { 84 | "axios": "0.21.0", 85 | "balloon-css": "^1.0.3", 86 | "promise-polyfill": "8.2.0", 87 | "get-browser-rtc": "^1.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/assets/airplay.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/comment-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/full-web.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/assets/pallette.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/subtitle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/volume-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/volume-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/css/balloon.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/balloon-css/balloon.css'; 2 | 3 | [data-balloon]:before { 4 | display: none; 5 | } 6 | 7 | [data-balloon]:after { 8 | padding: 0.3em 0.7em; 9 | background: rgba(17, 17, 17, 0.7); 10 | } 11 | 12 | [data-balloon][data-balloon-pos="up"]:after { 13 | margin-bottom: 0; 14 | } -------------------------------------------------------------------------------- /src/css/bezel.scss: -------------------------------------------------------------------------------- 1 | .dplayer-bezel { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | font-size: 22px; 8 | color: #fff; 9 | pointer-events: none; 10 | .dplayer-bezel-icon { 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | margin: -26px 0 0 -26px; 15 | height: 52px; 16 | width: 52px; 17 | padding: 12px; 18 | box-sizing: border-box; 19 | background: rgba(0, 0, 0, .5); 20 | border-radius: 50%; 21 | opacity: 0; 22 | pointer-events: none; 23 | &.dplayer-bezel-transition { 24 | animation: bezel-hide .5s linear; 25 | } 26 | @keyframes bezel-hide { 27 | from { 28 | opacity: 1; 29 | transform: scale(1); 30 | } 31 | to { 32 | opacity: 0; 33 | transform: scale(2); 34 | } 35 | } 36 | } 37 | .dplayer-danloading { 38 | position: absolute; 39 | top: 50%; 40 | margin-top: -7px; 41 | width: 100%; 42 | text-align: center; 43 | font-size: 14px; 44 | line-height: 14px; 45 | animation: my-face 5s infinite ease-in-out; 46 | } 47 | .diplayer-loading-icon { 48 | display: none; 49 | position: absolute; 50 | top: 50%; 51 | left: 50%; 52 | margin: -18px 0 0 -18px; 53 | height: 36px; 54 | width: 36px; 55 | pointer-events: none; 56 | .diplayer-loading-hide { 57 | display: none; 58 | } 59 | .diplayer-loading-dot { 60 | animation: diplayer-loading-dot-fade .8s ease infinite; 61 | opacity: 0; 62 | transform-origin: 4px 4px; 63 | @for $i from 7 through 1 { 64 | &.diplayer-loading-dot-#{$i} { 65 | animation-delay: .1s * $i; 66 | } 67 | } 68 | } 69 | @keyframes diplayer-loading-dot-fade { 70 | 0% { 71 | opacity: .7; 72 | transform: scale(1.2, 1.2) 73 | } 74 | 50% { 75 | opacity: .25; 76 | transform: scale(.9, .9) 77 | } 78 | to { 79 | opacity: .25; 80 | transform: scale(.85, .85) 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/css/controller.scss: -------------------------------------------------------------------------------- 1 | .dplayer-controller-mask { 2 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==) repeat-x bottom; 3 | height: 98px; 4 | width: 100%; 5 | position: absolute; 6 | bottom: 0; 7 | transition: all 0.3s ease; 8 | } 9 | 10 | .dplayer-controller { 11 | position: absolute; 12 | bottom: 0; 13 | left: 0; 14 | right: 0; 15 | height: 41px; 16 | padding: 0 20px; 17 | user-select: none; 18 | transition: all 0.3s ease; 19 | &.dplayer-controller-comment { 20 | .dplayer-icons { 21 | display: none; 22 | } 23 | .dplayer-icons.dplayer-comment-box { 24 | display: block; 25 | } 26 | } 27 | .dplayer-bar-wrap { 28 | padding: 5px 0; 29 | cursor: pointer; 30 | position: absolute; 31 | bottom: 33px; 32 | width: calc(100% - 40px); 33 | height: 3px; 34 | &:hover { 35 | .dplayer-bar .dplayer-played .dplayer-thumb { 36 | transform: scale(1); 37 | } 38 | .dplayer-highlight { 39 | display: block; 40 | width: 8px; 41 | transform: translateX(-4px); 42 | top: 4px; 43 | height: 40%; 44 | } 45 | } 46 | .dplayer-highlight { 47 | z-index: 12; 48 | position: absolute; 49 | top: 5px; 50 | width: 6px; 51 | height: 20%; 52 | border-radius: 6px; 53 | background-color: #fff; 54 | text-align: center; 55 | transform: translateX(-3px); 56 | transition: all .2s ease-in-out; 57 | &:hover { 58 | .dplayer-highlight-text { 59 | display: block; 60 | } 61 | &~.dplayer-bar-preview { 62 | opacity: 0; 63 | } 64 | &~.dplayer-bar-time { 65 | opacity: 0; 66 | } 67 | } 68 | .dplayer-highlight-text { 69 | display: none; 70 | position: absolute; 71 | left: 50%; 72 | top: -24px; 73 | padding: 5px 8px; 74 | background-color: rgba(0, 0, 0, .62); 75 | color: #fff; 76 | border-radius: 4px; 77 | font-size: 12px; 78 | white-space: nowrap; 79 | transform: translateX(-50%); 80 | } 81 | } 82 | .dplayer-bar-preview { 83 | position: absolute; 84 | background: #fff; 85 | pointer-events: none; 86 | display: none; 87 | background-size: 16000px 100%; 88 | } 89 | .dplayer-bar-preview-canvas { 90 | position: absolute; 91 | width: 100%; 92 | height: 100%; 93 | z-index: 1; 94 | pointer-events: none; 95 | } 96 | .dplayer-bar-time { 97 | &.hidden { 98 | opacity: 0; 99 | } 100 | position: absolute; 101 | left: 0px; 102 | top: -20px; 103 | border-radius: 4px; 104 | padding: 5px 7px; 105 | background-color: rgba(0, 0, 0, 0.62); 106 | color: #fff; 107 | font-size: 12px; 108 | text-align: center; 109 | opacity: 1; 110 | transition: opacity .1s ease-in-out; 111 | word-wrap: normal; 112 | word-break: normal; 113 | z-index: 2; 114 | pointer-events: none; 115 | } 116 | .dplayer-bar { 117 | position: relative; 118 | height: 3px; 119 | width: 100%; 120 | background: rgba(255, 255, 255, .2); 121 | cursor: pointer; 122 | .dplayer-loaded { 123 | position: absolute; 124 | left: 0; 125 | top: 0; 126 | bottom: 0; 127 | background: rgba(255, 255, 255, .4); 128 | height: 3px; 129 | transition: all 0.5s ease; 130 | will-change: width; 131 | } 132 | .dplayer-played { 133 | position: absolute; 134 | left: 0; 135 | top: 0; 136 | bottom: 0; 137 | height: 3px; 138 | will-change: width; 139 | .dplayer-thumb { 140 | position: absolute; 141 | top: 0; 142 | right: 5px; 143 | margin-top: -4px; 144 | margin-right: -10px; 145 | height: 11px; 146 | width: 11px; 147 | border-radius: 50%; 148 | cursor: pointer; 149 | transition: all .3s ease-in-out; 150 | transform: scale(0); 151 | } 152 | } 153 | } 154 | } 155 | .dplayer-icons { 156 | height: 38px; 157 | position: absolute; 158 | bottom: 0; 159 | &.dplayer-comment-box { 160 | display: none; 161 | position: absolute; 162 | transition: all .3s ease-in-out; 163 | z-index: 2; 164 | height: 38px; 165 | bottom: 0; 166 | left: 20px; 167 | right: 20px; 168 | color: #fff; 169 | .dplayer-icon { 170 | padding: 7px; 171 | } 172 | .dplayer-comment-setting-icon { 173 | position: absolute; 174 | left: 0; 175 | top: 0; 176 | } 177 | .dplayer-send-icon { 178 | position: absolute; 179 | right: 0; 180 | top: 0; 181 | } 182 | .dplayer-comment-setting-box { 183 | position: absolute; 184 | background: rgba(28, 28, 28, 0.9); 185 | bottom: 41px; 186 | left: 0; 187 | box-shadow: 0 0 25px rgba(0, 0, 0, .3); 188 | border-radius: 4px; 189 | padding: 10px 10px 16px; 190 | font-size: 14px; 191 | width: 204px; 192 | transition: all .3s ease-in-out; 193 | transform: scale(0); 194 | &.dplayer-comment-setting-open { 195 | transform: scale(1); 196 | } 197 | input[type=radio] { 198 | display: none; 199 | } 200 | label { 201 | cursor: pointer; 202 | } 203 | .dplayer-comment-setting-title { 204 | font-size: 13px; 205 | color: #fff; 206 | line-height: 30px; 207 | } 208 | .dplayer-comment-setting-type { 209 | font-size: 0; 210 | .dplayer-comment-setting-title { 211 | margin-bottom: 6px; 212 | } 213 | label { 214 | &:nth-child(2) { 215 | span { 216 | border-radius: 4px 0 0 4px; 217 | } 218 | } 219 | &:nth-child(4) { 220 | span { 221 | border-radius: 0 4px 4px 0; 222 | } 223 | } 224 | } 225 | span { 226 | width: 33%; 227 | padding: 4px 6px; 228 | line-height: 16px; 229 | display: inline-block; 230 | font-size: 12px; 231 | color: #fff; 232 | border: 1px solid #fff; 233 | margin-right: -1px; 234 | box-sizing: border-box; 235 | text-align: center; 236 | cursor: pointer; 237 | } 238 | input:checked+span { 239 | background: #E4E4E6; 240 | color: #1c1c1c; 241 | } 242 | } 243 | .dplayer-comment-setting-color { 244 | font-size: 0; 245 | label { 246 | font-size: 0; 247 | padding: 6px; 248 | display: inline-block; 249 | } 250 | span { 251 | width: 22px; 252 | height: 22px; 253 | display: inline-block; 254 | border-radius: 50%; 255 | box-sizing: border-box; 256 | cursor: pointer; 257 | &:hover { 258 | animation: my-face 5s infinite ease-in-out; 259 | } 260 | } 261 | } 262 | } 263 | .dplayer-comment-input { 264 | outline: none; 265 | border: none; 266 | padding: 8px 31px; 267 | font-size: 14px; 268 | line-height: 18px; 269 | text-align: center; 270 | border-radius: 4px; 271 | background: none; 272 | margin: 0; 273 | height: 100%; 274 | box-sizing: border-box; 275 | width: 100%; 276 | color: #fff; 277 | &::placeholder { 278 | color: #fff; 279 | opacity: 0.8; 280 | } 281 | &::-ms-clear { 282 | display: none; 283 | } 284 | } 285 | } 286 | &.dplayer-icons-left { 287 | .dplayer-icon { 288 | padding: 7px; 289 | } 290 | } 291 | &.dplayer-icons-right { 292 | right: 20px; 293 | .dplayer-icon { 294 | padding: 8px; 295 | } 296 | } 297 | .dplayer-time, 298 | .dplayer-live-badge { 299 | line-height: 38px; 300 | color: #eee; 301 | text-shadow: 0 0 2px rgba(0, 0, 0, .5); 302 | vertical-align: middle; 303 | font-size: 13px; 304 | cursor: default; 305 | } 306 | .dplayer-live-dot { 307 | display: inline-block; 308 | width: 6px; 309 | height: 6px; 310 | vertical-align: 4%; 311 | margin-right: 5px; 312 | content: ''; 313 | border-radius: 6px; 314 | } 315 | .dplayer-icon { 316 | width: 40px; 317 | height: 100%; 318 | border: none; 319 | background-color: transparent; 320 | outline: none; 321 | cursor: pointer; 322 | vertical-align: middle; 323 | box-sizing: border-box; 324 | display: inline-block; 325 | .dplayer-icon-content { 326 | transition: all .2s ease-in-out; 327 | opacity: .8; 328 | } 329 | &:hover { 330 | .dplayer-icon-content { 331 | opacity: 1; 332 | } 333 | } 334 | &.dplayer-quality-icon { 335 | color: #fff; 336 | width: auto; 337 | line-height: 22px; 338 | font-size: 14px; 339 | } 340 | &.dplayer-comment-icon { 341 | padding: 10px 9px 9px; 342 | } 343 | &.dplayer-setting-icon { 344 | padding-top: 8.5px; 345 | } 346 | &.dplayer-volume-icon { 347 | width: 43px; 348 | } 349 | } 350 | .dplayer-volume { 351 | position: relative; 352 | display: inline-block; 353 | cursor: pointer; 354 | height: 100%; 355 | &:hover { 356 | .dplayer-volume-bar-wrap .dplayer-volume-bar { 357 | width: 45px; 358 | } 359 | .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner .dplayer-thumb { 360 | transform: scale(1); 361 | } 362 | } 363 | &.dplayer-volume-active { 364 | .dplayer-volume-bar-wrap .dplayer-volume-bar { 365 | width: 45px; 366 | } 367 | .dplayer-volume-bar-wrap .dplayer-volume-bar .dplayer-volume-bar-inner .dplayer-thumb { 368 | transform: scale(1); 369 | } 370 | } 371 | .dplayer-volume-bar-wrap { 372 | display: inline-block; 373 | margin: 0 10px 0 -5px; 374 | vertical-align: middle; 375 | height: 100%; 376 | .dplayer-volume-bar { 377 | position: relative; 378 | top: 17px; 379 | width: 0; 380 | height: 3px; 381 | background: #aaa; 382 | transition: all 0.3s ease-in-out; 383 | .dplayer-volume-bar-inner { 384 | position: absolute; 385 | bottom: 0; 386 | left: 0; 387 | height: 100%; 388 | transition: all 0.1s ease; 389 | will-change: width; 390 | .dplayer-thumb { 391 | position: absolute; 392 | top: 0; 393 | right: 5px; 394 | margin-top: -4px; 395 | margin-right: -10px; 396 | height: 11px; 397 | width: 11px; 398 | border-radius: 50%; 399 | cursor: pointer; 400 | transition: all .3s ease-in-out; 401 | transform: scale(0); 402 | } 403 | } 404 | } 405 | } 406 | } 407 | .dplayer-subtitle-btn { 408 | display: inline-block; 409 | height: 100%; 410 | } 411 | .dplayer-setting { 412 | display: inline-block; 413 | height: 100%; 414 | .dplayer-setting-box { 415 | position: absolute; 416 | right: 0; 417 | bottom: 50px; 418 | transform: scale(0); 419 | width: 150px; 420 | border-radius: 2px; 421 | background: rgba(28, 28, 28, 0.9); 422 | padding: 7px 0; 423 | transition: all .3s ease-in-out; 424 | overflow: hidden; 425 | z-index: 2; 426 | &>div { 427 | display: none; 428 | &.dplayer-setting-origin-panel { 429 | display: block; 430 | } 431 | } 432 | &.dplayer-setting-box-open { 433 | transform: scale(1); 434 | } 435 | &.dplayer-setting-box-narrow { 436 | width: 70px; 437 | text-align: center; 438 | } 439 | &.dplayer-setting-box-speed { 440 | .dplayer-setting-origin-panel { 441 | display: none; 442 | } 443 | .dplayer-setting-speed-panel { 444 | display: block; 445 | } 446 | } 447 | } 448 | .dplayer-setting-item, 449 | .dplayer-setting-speed-item { 450 | height: 30px; 451 | padding: 5px 10px; 452 | box-sizing: border-box; 453 | cursor: pointer; 454 | position: relative; 455 | &:hover { 456 | background-color: rgba(255, 255, 255, .1); 457 | } 458 | } 459 | .dplayer-setting-danmaku { 460 | padding: 5px 0; 461 | .dplayer-label { 462 | padding: 0 10px; 463 | display: inline; 464 | } 465 | &:hover { 466 | .dplayer-label { 467 | display: none; 468 | } 469 | .dplayer-danmaku-bar-wrap { 470 | display: inline-block; 471 | } 472 | } 473 | &.dplayer-setting-danmaku-active { 474 | .dplayer-label { 475 | display: none; 476 | } 477 | .dplayer-danmaku-bar-wrap { 478 | display: inline-block; 479 | } 480 | } 481 | .dplayer-danmaku-bar-wrap { 482 | padding: 0 10px; 483 | box-sizing: border-box; 484 | display: none; 485 | vertical-align: middle; 486 | height: 100%; 487 | width: 100%; 488 | .dplayer-danmaku-bar { 489 | position: relative; 490 | top: 8.5px; 491 | width: 100%; 492 | height: 3px; 493 | background: #fff; 494 | transition: all 0.3s ease-in-out; 495 | .dplayer-danmaku-bar-inner { 496 | position: absolute; 497 | bottom: 0; 498 | left: 0; 499 | height: 100%; 500 | transition: all 0.1s ease; 501 | background: #aaa; 502 | will-change: width; 503 | .dplayer-thumb { 504 | position: absolute; 505 | top: 0; 506 | right: 5px; 507 | margin-top: -4px; 508 | margin-right: -10px; 509 | height: 11px; 510 | width: 11px; 511 | border-radius: 50%; 512 | cursor: pointer; 513 | transition: all .3s ease-in-out; 514 | background: #aaa; 515 | } 516 | } 517 | } 518 | } 519 | } 520 | } 521 | .dplayer-full { 522 | display: inline-block; 523 | height: 100%; 524 | position: relative; 525 | &:hover { 526 | .dplayer-full-in-icon { 527 | display: block; 528 | } 529 | } 530 | .dplayer-full-in-icon { 531 | position: absolute; 532 | top: -30px; 533 | z-index: 1; 534 | display: none; 535 | } 536 | } 537 | .dplayer-quality { 538 | position: relative; 539 | display: inline-block; 540 | height: 100%; 541 | z-index: 2; 542 | &:hover { 543 | .dplayer-quality-list { 544 | display: block; 545 | } 546 | .dplayer-quality-mask { 547 | display: block; 548 | } 549 | } 550 | .dplayer-quality-mask { 551 | display: none; 552 | position: absolute; 553 | bottom: 38px; 554 | left: -18px; 555 | width: 80px; 556 | padding-bottom: 12px; 557 | } 558 | .dplayer-quality-list { 559 | display: none; 560 | font-size: 12px; 561 | width: 80px; 562 | border-radius: 2px; 563 | background: rgba(28, 28, 28, 0.9); 564 | padding: 5px 0; 565 | transition: all .3s ease-in-out; 566 | overflow: hidden; 567 | color: #fff; 568 | text-align: center; 569 | } 570 | .dplayer-quality-item { 571 | height: 25px; 572 | box-sizing: border-box; 573 | cursor: pointer; 574 | line-height: 25px; 575 | &:hover { 576 | background-color: rgba(255, 255, 255, .1); 577 | } 578 | } 579 | } 580 | .dplayer-comment { 581 | display: inline-block; 582 | height: 100%; 583 | } 584 | .dplayer-label { 585 | color: #eee; 586 | font-size: 13px; 587 | display: inline-block; 588 | vertical-align: middle; 589 | white-space: nowrap; 590 | } 591 | .dplayer-toggle { 592 | width: 32px; 593 | height: 20px; 594 | text-align: center; 595 | font-size: 0; 596 | vertical-align: middle; 597 | position: absolute; 598 | top: 5px; 599 | right: 10px; 600 | input { 601 | max-height: 0; 602 | max-width: 0; 603 | display: none; 604 | } 605 | input+label { 606 | display: inline-block; 607 | position: relative; 608 | box-shadow: rgb(223, 223, 223) 0 0 0 0 inset; 609 | border: 1px solid rgb(223, 223, 223); 610 | height: 20px; 611 | width: 32px; 612 | border-radius: 10px; 613 | box-sizing: border-box; 614 | cursor: pointer; 615 | transition: .2s ease-in-out; 616 | } 617 | input+label:before { 618 | content: ""; 619 | position: absolute; 620 | display: block; 621 | height: 18px; 622 | width: 18px; 623 | top: 0; 624 | left: 0; 625 | border-radius: 15px; 626 | transition: .2s ease-in-out; 627 | } 628 | input+label:after { 629 | content: ""; 630 | position: absolute; 631 | display: block; 632 | left: 0; 633 | top: 0; 634 | border-radius: 15px; 635 | background: #fff; 636 | transition: .2s ease-in-out; 637 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); 638 | height: 18px; 639 | width: 18px; 640 | } 641 | input:checked+label { 642 | border-color: rgba(255, 255, 255, 0.5); 643 | } 644 | input:checked+label:before { 645 | width: 30px; 646 | background: rgba(255, 255, 255, 0.5); 647 | } 648 | input:checked+label:after { 649 | left: 12px; 650 | } 651 | } 652 | } 653 | } 654 | 655 | .dplayer-mobile-play { 656 | display: none; 657 | width: 50px; 658 | height: 50px; 659 | border: none; 660 | background-color: transparent; 661 | outline: none; 662 | cursor: pointer; 663 | box-sizing: border-box; 664 | position: absolute; 665 | bottom: 0; 666 | opacity: 0.8; 667 | position: absolute; 668 | left: 50%; 669 | top: 50%; 670 | transform: translate(-50%, -50%); 671 | } -------------------------------------------------------------------------------- /src/css/danmaku.scss: -------------------------------------------------------------------------------- 1 | .dplayer-danmaku { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | font-size: 22px; 8 | color: #fff; 9 | .dplayer-danmaku-item { 10 | display: inline-block; 11 | pointer-events: none; 12 | user-select: none; 13 | cursor: default; 14 | white-space: nowrap; 15 | text-shadow: .5px .5px .5px rgba(0, 0, 0, .5); 16 | &--demo { 17 | position: absolute; 18 | visibility: hidden; 19 | } 20 | } 21 | .dplayer-danmaku-right { 22 | position: absolute; 23 | right: 0; 24 | transform: translateX(100%); 25 | &.dplayer-danmaku-move { 26 | will-change: transform; 27 | animation: danmaku 5s linear; 28 | animation-play-state: paused; 29 | } 30 | } 31 | @keyframes danmaku { 32 | from { 33 | transform: translateX(100%); 34 | } 35 | } 36 | .dplayer-danmaku-top, 37 | .dplayer-danmaku-bottom { 38 | position: absolute; 39 | width: 100%; 40 | text-align: center; 41 | visibility: hidden; 42 | &.dplayer-danmaku-move { 43 | will-change: visibility; 44 | animation: danmaku-center 4s linear; 45 | animation-play-state: paused; 46 | } 47 | } 48 | @keyframes danmaku-center { 49 | from { 50 | visibility: visible; 51 | } 52 | to { 53 | visibility: visible; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/css/global.scss: -------------------------------------------------------------------------------- 1 | @keyframes my-face { 2 | 2% { 3 | transform: translate(0, 1.5px) rotate(1.5deg); 4 | } 5 | 4% { 6 | transform: translate(0, -1.5px) rotate(-0.5deg); 7 | } 8 | 6% { 9 | transform: translate(0, 1.5px) rotate(-1.5deg); 10 | } 11 | 8% { 12 | transform: translate(0, -1.5px) rotate(-1.5deg); 13 | } 14 | 10% { 15 | transform: translate(0, 2.5px) rotate(1.5deg); 16 | } 17 | 12% { 18 | transform: translate(0, -0.5px) rotate(1.5deg); 19 | } 20 | 14% { 21 | transform: translate(0, -1.5px) rotate(1.5deg); 22 | } 23 | 16% { 24 | transform: translate(0, -0.5px) rotate(-1.5deg); 25 | } 26 | 18% { 27 | transform: translate(0, 0.5px) rotate(-1.5deg); 28 | } 29 | 20% { 30 | transform: translate(0, -1.5px) rotate(2.5deg); 31 | } 32 | 22% { 33 | transform: translate(0, 0.5px) rotate(-1.5deg); 34 | } 35 | 24% { 36 | transform: translate(0, 1.5px) rotate(1.5deg); 37 | } 38 | 26% { 39 | transform: translate(0, 0.5px) rotate(0.5deg); 40 | } 41 | 28% { 42 | transform: translate(0, 0.5px) rotate(1.5deg); 43 | } 44 | 30% { 45 | transform: translate(0, -0.5px) rotate(2.5deg); 46 | } 47 | 32% { 48 | transform: translate(0, 1.5px) rotate(-0.5deg); 49 | } 50 | 34% { 51 | transform: translate(0, 1.5px) rotate(-0.5deg); 52 | } 53 | 36% { 54 | transform: translate(0, -1.5px) rotate(2.5deg); 55 | } 56 | 38% { 57 | transform: translate(0, 1.5px) rotate(-1.5deg); 58 | } 59 | 40% { 60 | transform: translate(0, -0.5px) rotate(2.5deg); 61 | } 62 | 42% { 63 | transform: translate(0, 2.5px) rotate(-1.5deg); 64 | } 65 | 44% { 66 | transform: translate(0, 1.5px) rotate(0.5deg); 67 | } 68 | 46% { 69 | transform: translate(0, -1.5px) rotate(2.5deg); 70 | } 71 | 48% { 72 | transform: translate(0, -0.5px) rotate(0.5deg); 73 | } 74 | 50% { 75 | transform: translate(0, 0.5px) rotate(0.5deg); 76 | } 77 | 52% { 78 | transform: translate(0, 2.5px) rotate(2.5deg); 79 | } 80 | 54% { 81 | transform: translate(0, -1.5px) rotate(1.5deg); 82 | } 83 | 56% { 84 | transform: translate(0, 2.5px) rotate(2.5deg); 85 | } 86 | 58% { 87 | transform: translate(0, 0.5px) rotate(2.5deg); 88 | } 89 | 60% { 90 | transform: translate(0, 2.5px) rotate(2.5deg); 91 | } 92 | 62% { 93 | transform: translate(0, -0.5px) rotate(2.5deg); 94 | } 95 | 64% { 96 | transform: translate(0, -0.5px) rotate(1.5deg); 97 | } 98 | 66% { 99 | transform: translate(0, 1.5px) rotate(-0.5deg); 100 | } 101 | 68% { 102 | transform: translate(0, -1.5px) rotate(-0.5deg); 103 | } 104 | 70% { 105 | transform: translate(0, 1.5px) rotate(0.5deg); 106 | } 107 | 72% { 108 | transform: translate(0, 2.5px) rotate(1.5deg); 109 | } 110 | 74% { 111 | transform: translate(0, -0.5px) rotate(0.5deg); 112 | } 113 | 76% { 114 | transform: translate(0, -0.5px) rotate(2.5deg); 115 | } 116 | 78% { 117 | transform: translate(0, -0.5px) rotate(1.5deg); 118 | } 119 | 80% { 120 | transform: translate(0, 1.5px) rotate(1.5deg); 121 | } 122 | 82% { 123 | transform: translate(0, -0.5px) rotate(0.5deg); 124 | } 125 | 84% { 126 | transform: translate(0, 1.5px) rotate(2.5deg); 127 | } 128 | 86% { 129 | transform: translate(0, -1.5px) rotate(-1.5deg); 130 | } 131 | 88% { 132 | transform: translate(0, -0.5px) rotate(2.5deg); 133 | } 134 | 90% { 135 | transform: translate(0, 2.5px) rotate(-0.5deg); 136 | } 137 | 92% { 138 | transform: translate(0, 0.5px) rotate(-0.5deg); 139 | } 140 | 94% { 141 | transform: translate(0, 2.5px) rotate(0.5deg); 142 | } 143 | 96% { 144 | transform: translate(0, -0.5px) rotate(1.5deg); 145 | } 146 | 98% { 147 | transform: translate(0, -1.5px) rotate(-0.5deg); 148 | } 149 | 0%, 150 | 100% { 151 | transform: translate(0, 0) rotate(0deg); 152 | } 153 | } -------------------------------------------------------------------------------- /src/css/index.scss: -------------------------------------------------------------------------------- 1 | @import './global'; 2 | @import './player'; 3 | @import './balloon'; 4 | @import './bezel'; 5 | @import './controller'; 6 | @import './danmaku'; 7 | @import './logo'; 8 | @import './menu'; 9 | @import './notice'; 10 | @import './subtitle'; 11 | @import './video'; 12 | @import './info-panel'; -------------------------------------------------------------------------------- /src/css/info-panel.scss: -------------------------------------------------------------------------------- 1 | .dplayer-info-panel { 2 | position: absolute; 3 | top: 10px; 4 | left: 10px; 5 | width: 400px; 6 | background: rgba(28, 28, 28, 0.8); 7 | padding: 10px; 8 | color: #fff; 9 | font-size: 12px; 10 | border-radius: 2px; 11 | 12 | &-hide { 13 | display: none; 14 | } 15 | 16 | .dplayer-info-panel-close { 17 | cursor: pointer; 18 | position: absolute; 19 | right: 10px; 20 | top: 10px; 21 | } 22 | 23 | .dplayer-info-panel-item { 24 | & > span { 25 | display: inline-block; 26 | vertical-align: middle; 27 | line-height: 15px; 28 | white-space: nowrap; 29 | text-overflow: ellipsis; 30 | overflow: hidden; 31 | } 32 | } 33 | 34 | .dplayer-info-panel-item-title { 35 | width: 100px; 36 | text-align: right; 37 | margin-right: 10px; 38 | } 39 | 40 | .dplayer-info-panel-item-data { 41 | width: 260px; 42 | } 43 | } -------------------------------------------------------------------------------- /src/css/logo.scss: -------------------------------------------------------------------------------- 1 | .dplayer-logo { 2 | pointer-events: none; 3 | position: absolute; 4 | left: 20px; 5 | top: 20px; 6 | max-width: 50px; 7 | max-height: 50px; 8 | img { 9 | max-width: 100%; 10 | max-height: 100%; 11 | background: none; 12 | } 13 | } -------------------------------------------------------------------------------- /src/css/menu.scss: -------------------------------------------------------------------------------- 1 | .dplayer-menu { 2 | position: absolute; 3 | width: 170px; 4 | border-radius: 2px; 5 | background: rgba(28, 28, 28, 0.85); 6 | padding: 5px 0; 7 | overflow: hidden; 8 | z-index: 3; 9 | display: none; 10 | &.dplayer-menu-show { 11 | display: block; 12 | } 13 | .dplayer-menu-item { 14 | height: 30px; 15 | box-sizing: border-box; 16 | cursor: pointer; 17 | &:hover { 18 | background-color: rgba(255, 255, 255, .1); 19 | } 20 | a { 21 | display: inline-block; 22 | padding: 0 10px; 23 | line-height: 30px; 24 | color: #eee; 25 | font-size: 13px; 26 | display: inline-block; 27 | vertical-align: middle; 28 | width: 100%; 29 | box-sizing: border-box; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | overflow: hidden; 33 | &:hover { 34 | text-decoration: none; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/css/notice.scss: -------------------------------------------------------------------------------- 1 | .dplayer-notice { 2 | opacity: 0; 3 | position: absolute; 4 | bottom: 60px; 5 | left: 20px; 6 | font-size: 14px; 7 | border-radius: 2px; 8 | background: rgba(28, 28, 28, 0.9); 9 | padding: 7px 20px; 10 | transition: all .3s ease-in-out; 11 | overflow: hidden; 12 | color: #fff; 13 | pointer-events: none; 14 | } -------------------------------------------------------------------------------- /src/css/player.scss: -------------------------------------------------------------------------------- 1 | .dplayer { 2 | position: relative; 3 | overflow: hidden; 4 | user-select: none; 5 | line-height: 1; 6 | 7 | * { 8 | box-sizing: content-box; 9 | } 10 | 11 | svg { 12 | width: 100%; 13 | height: 100%; 14 | 15 | path, 16 | circle { 17 | fill: #fff; 18 | } 19 | } 20 | 21 | &:-webkit-full-screen { 22 | width: 100%; 23 | height: 100%; 24 | background: #000; 25 | position: fixed; 26 | z-index: 100000; 27 | left: 0; 28 | top: 0; 29 | margin: 0; 30 | padding: 0; 31 | transform: translate(0, 0); 32 | 33 | .dplayer-danmaku { 34 | .dplayer-danmaku-top, 35 | .dplayer-danmaku-bottom { 36 | &.dplayer-danmaku-move { 37 | animation: danmaku-center 6s linear; 38 | animation-play-state: inherit; 39 | } 40 | } 41 | 42 | .dplayer-danmaku-right { 43 | &.dplayer-danmaku-move { 44 | animation: danmaku 8s linear; 45 | animation-play-state: inherit; 46 | } 47 | } 48 | } 49 | } 50 | 51 | &.dplayer-no-danmaku { 52 | .dplayer-controller .dplayer-icons .dplayer-setting .dplayer-setting-box { 53 | .dplayer-setting-showdan, 54 | .dplayer-setting-danmaku, 55 | .dplayer-setting-danunlimit { 56 | display: none; 57 | } 58 | } 59 | 60 | .dplayer-controller .dplayer-icons .dplayer-comment { 61 | display: none; 62 | } 63 | 64 | .dplayer-danmaku { 65 | display: none; 66 | } 67 | } 68 | 69 | &.dplayer-live { 70 | .dplayer-time { 71 | display: none; 72 | } 73 | .dplayer-bar-wrap { 74 | display: none; 75 | } 76 | .dplayer-setting-speed { 77 | display: none; 78 | } 79 | .dplayer-setting-loop { 80 | display: none; 81 | } 82 | 83 | &.dplayer-no-danmaku { 84 | .dplayer-setting { 85 | display: none; 86 | } 87 | } 88 | } 89 | 90 | &.dplayer-arrow { 91 | .dplayer-danmaku { 92 | font-size: 18px; 93 | } 94 | .dplayer-icon { 95 | margin: 0 -3px; 96 | } 97 | } 98 | 99 | &.dplayer-playing { 100 | .dplayer-danmaku .dplayer-danmaku-move { 101 | animation-play-state: running; 102 | } 103 | 104 | @media (min-width: 900px) { 105 | .dplayer-controller-mask { 106 | opacity: 0; 107 | } 108 | .dplayer-controller { 109 | opacity: 0; 110 | } 111 | 112 | &:hover { 113 | .dplayer-controller-mask { 114 | opacity: 1; 115 | } 116 | .dplayer-controller { 117 | opacity: 1; 118 | } 119 | } 120 | } 121 | } 122 | 123 | &.dplayer-loading { 124 | .dplayer-bezel .diplayer-loading-icon { 125 | display: block; 126 | } 127 | } 128 | 129 | &.dplayer-loading, 130 | &.dplayer-paused { 131 | .dplayer-danmaku, 132 | .dplayer-danmaku-move { 133 | animation-play-state: paused; 134 | } 135 | } 136 | 137 | &.dplayer-hide-controller { 138 | cursor: none; 139 | 140 | .dplayer-controller-mask { 141 | opacity: 0; 142 | transform: translateY(100%); 143 | } 144 | .dplayer-controller { 145 | opacity: 0; 146 | transform: translateY(100%); 147 | } 148 | } 149 | &.dplayer-show-controller { 150 | .dplayer-controller-mask { 151 | opacity: 1; 152 | } 153 | .dplayer-controller { 154 | opacity: 1; 155 | } 156 | } 157 | &.dplayer-fulled { 158 | position: fixed; 159 | z-index: 100000; 160 | left: 0; 161 | top: 0; 162 | width: 100% !important; 163 | height: 100% !important; 164 | } 165 | &.dplayer-mobile { 166 | .dplayer-controller .dplayer-icons { 167 | .dplayer-volume, 168 | .dplayer-camera-icon, 169 | .dplayer-airplay-icon, 170 | .dplayer-play-icon { 171 | display: none; 172 | } 173 | .dplayer-full .dplayer-full-in-icon { 174 | position: static; 175 | display: inline-block; 176 | } 177 | } 178 | 179 | .dplayer-bar-time { 180 | display: none; 181 | } 182 | 183 | &.dplayer-hide-controller { 184 | .dplayer-mobile-play { 185 | display: none; 186 | } 187 | } 188 | 189 | .dplayer-mobile-play { 190 | display: block; 191 | } 192 | } 193 | } 194 | 195 | // To hide scroll bar, apply this class to 196 | .dplayer-web-fullscreen-fix { 197 | position: fixed; 198 | top: 0; 199 | left: 0; 200 | margin: 0; 201 | padding: 0; 202 | } 203 | -------------------------------------------------------------------------------- /src/css/subtitle.scss: -------------------------------------------------------------------------------- 1 | .dplayer-subtitle { 2 | position: absolute; 3 | bottom: 40px; 4 | width: 90%; 5 | left: 5%; 6 | text-align: center; 7 | color: #fff; 8 | text-shadow: 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.5); 9 | font-size: 20px; 10 | &.dplayer-subtitle-hide { 11 | display: none; 12 | } 13 | } -------------------------------------------------------------------------------- /src/css/video.scss: -------------------------------------------------------------------------------- 1 | .dplayer-mask { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: 1; 8 | display: none; 9 | &.dplayer-mask-show { 10 | display: block; 11 | } 12 | } 13 | 14 | .dplayer-video-wrap { 15 | position: relative; 16 | background: #000; 17 | font-size: 0; 18 | width: 100%; 19 | height: 100%; 20 | .dplayer-video { 21 | width: 100%; 22 | height: 100%; 23 | display: none; 24 | } 25 | .dplayer-video-current { 26 | display: block; 27 | } 28 | .dplayer-video-prepare { 29 | display: none; 30 | } 31 | } -------------------------------------------------------------------------------- /src/js/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default { 4 | send: (options) => { 5 | axios 6 | .post(options.url, options.data) 7 | .then((response) => { 8 | const data = response.data; 9 | if (!data || data.code !== 0) { 10 | options.error && options.error(data && data.msg); 11 | return; 12 | } 13 | options.success && options.success(data); 14 | }) 15 | .catch((e) => { 16 | console.error(e); 17 | options.error && options.error(); 18 | }); 19 | }, 20 | 21 | read: (options) => { 22 | axios 23 | .get(options.url) 24 | .then((response) => { 25 | const data = response.data; 26 | if (!data || data.code !== 0) { 27 | options.error && options.error(data && data.msg); 28 | return; 29 | } 30 | options.success && 31 | options.success( 32 | data.data.map((item) => ({ 33 | time: item[0], 34 | type: item[1], 35 | color: item[2], 36 | author: item[3], 37 | text: item[4], 38 | })) 39 | ); 40 | }) 41 | .catch((e) => { 42 | console.error(e); 43 | options.error && options.error(); 44 | }); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/js/bar.js: -------------------------------------------------------------------------------- 1 | class Bar { 2 | constructor(template) { 3 | this.elements = {}; 4 | this.elements.volume = template.volumeBar; 5 | this.elements.played = template.playedBar; 6 | this.elements.loaded = template.loadedBar; 7 | this.elements.danmaku = template.danmakuOpacityBar; 8 | } 9 | 10 | /** 11 | * Update progress 12 | * 13 | * @param {String} type - Point out which bar it is 14 | * @param {Number} percentage 15 | * @param {String} direction - Point out the direction of this bar, Should be height or width 16 | */ 17 | set(type, percentage, direction) { 18 | percentage = Math.max(percentage, 0); 19 | percentage = Math.min(percentage, 1); 20 | this.elements[type].style[direction] = percentage * 100 + '%'; 21 | } 22 | 23 | get(type) { 24 | return parseFloat(this.elements[type].style.width) / 100; 25 | } 26 | } 27 | 28 | export default Bar; 29 | -------------------------------------------------------------------------------- /src/js/bezel.js: -------------------------------------------------------------------------------- 1 | class Bezel { 2 | constructor(container) { 3 | this.container = container; 4 | 5 | this.container.addEventListener('animationend', () => { 6 | this.container.classList.remove('dplayer-bezel-transition'); 7 | }); 8 | } 9 | 10 | switch(icon) { 11 | this.container.innerHTML = icon; 12 | this.container.classList.add('dplayer-bezel-transition'); 13 | } 14 | } 15 | 16 | export default Bezel; 17 | -------------------------------------------------------------------------------- /src/js/comment.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | class Comment { 4 | constructor(player) { 5 | this.player = player; 6 | 7 | this.player.template.mask.addEventListener('click', () => { 8 | this.hide(); 9 | }); 10 | this.player.template.commentButton.addEventListener('click', () => { 11 | this.show(); 12 | }); 13 | this.player.template.commentSettingButton.addEventListener('click', () => { 14 | this.toggleSetting(); 15 | }); 16 | 17 | this.player.template.commentColorSettingBox.addEventListener('click', () => { 18 | const sele = this.player.template.commentColorSettingBox.querySelector('input:checked+span'); 19 | if (sele) { 20 | const color = this.player.template.commentColorSettingBox.querySelector('input:checked').value; 21 | this.player.template.commentSettingFill.style.fill = color; 22 | this.player.template.commentInput.style.color = color; 23 | this.player.template.commentSendFill.style.fill = color; 24 | } 25 | }); 26 | 27 | this.player.template.commentInput.addEventListener('click', () => { 28 | this.hideSetting(); 29 | }); 30 | this.player.template.commentInput.addEventListener('keydown', (e) => { 31 | const event = e || window.event; 32 | if (event.keyCode === 13) { 33 | this.send(); 34 | } 35 | }); 36 | 37 | this.player.template.commentSendButton.addEventListener('click', () => { 38 | this.send(); 39 | }); 40 | } 41 | 42 | show() { 43 | this.player.controller.disableAutoHide = true; 44 | this.player.template.controller.classList.add('dplayer-controller-comment'); 45 | this.player.template.mask.classList.add('dplayer-mask-show'); 46 | this.player.container.classList.add('dplayer-show-controller'); 47 | this.player.template.commentInput.focus(); 48 | } 49 | 50 | hide() { 51 | this.player.template.controller.classList.remove('dplayer-controller-comment'); 52 | this.player.template.mask.classList.remove('dplayer-mask-show'); 53 | this.player.container.classList.remove('dplayer-show-controller'); 54 | this.player.controller.disableAutoHide = false; 55 | this.hideSetting(); 56 | } 57 | 58 | showSetting() { 59 | this.player.template.commentSettingBox.classList.add('dplayer-comment-setting-open'); 60 | } 61 | 62 | hideSetting() { 63 | this.player.template.commentSettingBox.classList.remove('dplayer-comment-setting-open'); 64 | } 65 | 66 | toggleSetting() { 67 | if (this.player.template.commentSettingBox.classList.contains('dplayer-comment-setting-open')) { 68 | this.hideSetting(); 69 | } else { 70 | this.showSetting(); 71 | } 72 | } 73 | 74 | send() { 75 | this.player.template.commentInput.blur(); 76 | 77 | // text can't be empty 78 | if (!this.player.template.commentInput.value.replace(/^\s+|\s+$/g, '')) { 79 | this.player.notice(this.player.tran('Please input danmaku content!')); 80 | return; 81 | } 82 | 83 | this.player.danmaku.send( 84 | { 85 | text: this.player.template.commentInput.value, 86 | color: utils.color2Number(this.player.container.querySelector('.dplayer-comment-setting-color input:checked').value), 87 | type: parseInt(this.player.container.querySelector('.dplayer-comment-setting-type input:checked').value), 88 | }, 89 | () => { 90 | this.player.template.commentInput.value = ''; 91 | this.hide(); 92 | } 93 | ); 94 | } 95 | } 96 | 97 | export default Comment; 98 | -------------------------------------------------------------------------------- /src/js/contextmenu.js: -------------------------------------------------------------------------------- 1 | class ContextMenu { 2 | constructor(player) { 3 | this.player = player; 4 | this.shown = false; 5 | 6 | Array.prototype.slice.call(this.player.template.menuItem).forEach((item, index) => { 7 | if (this.player.options.contextmenu[index].click) { 8 | item.addEventListener('click', () => { 9 | this.player.options.contextmenu[index].click(this.player); 10 | this.hide(); 11 | }); 12 | } 13 | }); 14 | 15 | this.player.container.addEventListener('contextmenu', (e) => { 16 | if (this.shown) { 17 | this.hide(); 18 | return; 19 | } 20 | 21 | const event = e || window.event; 22 | event.preventDefault(); 23 | 24 | const clientRect = this.player.container.getBoundingClientRect(); 25 | this.show(event.clientX - clientRect.left, event.clientY - clientRect.top); 26 | 27 | this.player.template.mask.addEventListener('click', () => { 28 | this.hide(); 29 | }); 30 | }); 31 | } 32 | 33 | show(x, y) { 34 | this.player.template.menu.classList.add('dplayer-menu-show'); 35 | 36 | const clientRect = this.player.container.getBoundingClientRect(); 37 | if (x + this.player.template.menu.offsetWidth >= clientRect.width) { 38 | this.player.template.menu.style.right = clientRect.width - x + 'px'; 39 | this.player.template.menu.style.left = 'initial'; 40 | } else { 41 | this.player.template.menu.style.left = x + 'px'; 42 | this.player.template.menu.style.right = 'initial'; 43 | } 44 | if (y + this.player.template.menu.offsetHeight >= clientRect.height) { 45 | this.player.template.menu.style.bottom = clientRect.height - y + 'px'; 46 | this.player.template.menu.style.top = 'initial'; 47 | } else { 48 | this.player.template.menu.style.top = y + 'px'; 49 | this.player.template.menu.style.bottom = 'initial'; 50 | } 51 | 52 | this.player.template.mask.classList.add('dplayer-mask-show'); 53 | 54 | this.shown = true; 55 | this.player.events.trigger('contextmenu_show'); 56 | } 57 | 58 | hide() { 59 | this.player.template.mask.classList.remove('dplayer-mask-show'); 60 | this.player.template.menu.classList.remove('dplayer-menu-show'); 61 | 62 | this.shown = false; 63 | this.player.events.trigger('contextmenu_hide'); 64 | } 65 | } 66 | 67 | export default ContextMenu; 68 | -------------------------------------------------------------------------------- /src/js/controller.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | import Thumbnails from './thumbnails'; 3 | import Icons from './icons'; 4 | 5 | class Controller { 6 | constructor(player) { 7 | this.player = player; 8 | 9 | this.autoHideTimer = 0; 10 | // if (!utils.isMobile) { 11 | if (true) { 12 | this.player.container.addEventListener('mousemove', () => { 13 | this.setAutoHide(); 14 | }); 15 | this.player.container.addEventListener('click', () => { 16 | this.setAutoHide(); 17 | }); 18 | this.player.on('play', () => { 19 | this.setAutoHide(); 20 | }); 21 | this.player.on('pause', () => { 22 | this.setAutoHide(); 23 | }); 24 | } 25 | 26 | this.initPlayButton(); 27 | this.initThumbnails(); 28 | this.initPlayedBar(); 29 | this.initFullButton(); 30 | this.initQualityButton(); 31 | this.initScreenshotButton(); 32 | this.initSubtitleButton(); 33 | this.initHighlights(); 34 | this.initAirplayButton(); 35 | if (!utils.isMobile) { 36 | this.initVolumeButton(); 37 | } 38 | } 39 | 40 | initPlayButton() { 41 | this.player.template.playButton.addEventListener('click', () => { 42 | this.player.toggle(); 43 | }); 44 | 45 | this.player.template.mobilePlayButton.addEventListener('click', () => { 46 | this.player.toggle(); 47 | }); 48 | 49 | if (!utils.isMobile) { 50 | this.player.template.videoWrap.addEventListener('click', () => { 51 | this.player.toggle(); 52 | }); 53 | this.player.template.controllerMask.addEventListener('click', () => { 54 | this.player.toggle(); 55 | }); 56 | } else { 57 | this.player.template.videoWrap.addEventListener('click', () => { 58 | this.toggle(); 59 | }); 60 | this.player.template.controllerMask.addEventListener('click', () => { 61 | this.toggle(); 62 | }); 63 | } 64 | } 65 | 66 | initHighlights() { 67 | this.player.on('durationchange', () => { 68 | if (this.player.video.duration !== 1 && this.player.video.duration !== Infinity) { 69 | if (this.player.options.highlight) { 70 | const highlights = document.querySelectorAll('.dplayer-highlight'); 71 | [].slice.call(highlights, 0).forEach((item) => { 72 | this.player.template.playedBarWrap.removeChild(item); 73 | }); 74 | for (let i = 0; i < this.player.options.highlight.length; i++) { 75 | if (!this.player.options.highlight[i].text || !this.player.options.highlight[i].time) { 76 | continue; 77 | } 78 | const p = document.createElement('div'); 79 | p.classList.add('dplayer-highlight'); 80 | p.style.left = (this.player.options.highlight[i].time / this.player.video.duration) * 100 + '%'; 81 | p.innerHTML = '' + this.player.options.highlight[i].text + ''; 82 | this.player.template.playedBarWrap.insertBefore(p, this.player.template.playedBarTime); 83 | } 84 | } 85 | } 86 | }); 87 | } 88 | 89 | initThumbnails() { 90 | if (this.player.options.video.thumbnails) { 91 | this.thumbnails = new Thumbnails({ 92 | container: this.player.template.barPreview, 93 | barWidth: this.player.template.barWrap.offsetWidth, 94 | url: this.player.options.video.thumbnails, 95 | events: this.player.events, 96 | }); 97 | 98 | this.player.on('loadedmetadata', () => { 99 | this.thumbnails.resize(160, (this.player.video.videoHeight / this.player.video.videoWidth) * 160, this.player.template.barWrap.offsetWidth); 100 | }); 101 | } 102 | } 103 | 104 | initPlayedBar() { 105 | const thumbMove = (e) => { 106 | let percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.playedBarWrap)) / this.player.template.playedBarWrap.clientWidth; 107 | percentage = Math.max(percentage, 0); 108 | percentage = Math.min(percentage, 1); 109 | this.player.bar.set('played', percentage, 'width'); 110 | this.player.template.ptime.innerHTML = utils.secondToTime(percentage * this.player.video.duration); 111 | }; 112 | 113 | const thumbUp = (e) => { 114 | document.removeEventListener(utils.nameMap.dragEnd, thumbUp); 115 | document.removeEventListener(utils.nameMap.dragMove, thumbMove); 116 | let percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.playedBarWrap)) / this.player.template.playedBarWrap.clientWidth; 117 | percentage = Math.max(percentage, 0); 118 | percentage = Math.min(percentage, 1); 119 | this.player.bar.set('played', percentage, 'width'); 120 | this.player.seek(this.player.bar.get('played') * this.player.video.duration); 121 | this.player.timer.enable('progress'); 122 | }; 123 | 124 | this.player.template.playedBarWrap.addEventListener(utils.nameMap.dragStart, () => { 125 | this.player.timer.disable('progress'); 126 | document.addEventListener(utils.nameMap.dragMove, thumbMove); 127 | document.addEventListener(utils.nameMap.dragEnd, thumbUp); 128 | }); 129 | 130 | this.player.template.playedBarWrap.addEventListener(utils.nameMap.dragMove, (e) => { 131 | if (this.player.video.duration) { 132 | const px = this.player.template.playedBarWrap.getBoundingClientRect().left; 133 | const tx = (e.clientX || e.changedTouches[0].clientX) - px; 134 | if (tx < 0 || tx > this.player.template.playedBarWrap.offsetWidth) { 135 | return; 136 | } 137 | const time = this.player.video.duration * (tx / this.player.template.playedBarWrap.offsetWidth); 138 | if (utils.isMobile) { 139 | this.thumbnails && this.thumbnails.show(); 140 | } 141 | this.thumbnails && this.thumbnails.move(tx); 142 | this.player.template.playedBarTime.style.left = `${tx - (time >= 3600 ? 25 : 20)}px`; 143 | this.player.template.playedBarTime.innerText = utils.secondToTime(time); 144 | this.player.template.playedBarTime.classList.remove('hidden'); 145 | } 146 | }); 147 | 148 | this.player.template.playedBarWrap.addEventListener(utils.nameMap.dragEnd, () => { 149 | if (utils.isMobile) { 150 | this.thumbnails && this.thumbnails.hide(); 151 | } 152 | }); 153 | 154 | if (!utils.isMobile) { 155 | this.player.template.playedBarWrap.addEventListener('mouseenter', () => { 156 | if (this.player.video.duration) { 157 | this.thumbnails && this.thumbnails.show(); 158 | this.player.template.playedBarTime.classList.remove('hidden'); 159 | } 160 | }); 161 | 162 | this.player.template.playedBarWrap.addEventListener('mouseleave', () => { 163 | if (this.player.video.duration) { 164 | this.thumbnails && this.thumbnails.hide(); 165 | this.player.template.playedBarTime.classList.add('hidden'); 166 | } 167 | }); 168 | } 169 | } 170 | 171 | initFullButton() { 172 | this.player.template.browserFullButton.addEventListener('click', () => { 173 | this.player.fullScreen.toggle('browser'); 174 | }); 175 | 176 | this.player.template.webFullButton.addEventListener('click', () => { 177 | this.player.fullScreen.toggle('web'); 178 | }); 179 | } 180 | 181 | initVolumeButton() { 182 | const vWidth = 35; 183 | 184 | const volumeMove = (event) => { 185 | const e = event || window.event; 186 | const percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.volumeBarWrap) - 5.5) / vWidth; 187 | this.player.volume(percentage); 188 | }; 189 | const volumeUp = () => { 190 | document.removeEventListener(utils.nameMap.dragEnd, volumeUp); 191 | document.removeEventListener(utils.nameMap.dragMove, volumeMove); 192 | this.player.template.volumeButton.classList.remove('dplayer-volume-active'); 193 | }; 194 | 195 | this.player.template.volumeBarWrapWrap.addEventListener('click', (event) => { 196 | const e = event || window.event; 197 | const percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.volumeBarWrap) - 5.5) / vWidth; 198 | this.player.volume(percentage); 199 | }); 200 | this.player.template.volumeBarWrapWrap.addEventListener(utils.nameMap.dragStart, () => { 201 | document.addEventListener(utils.nameMap.dragMove, volumeMove); 202 | document.addEventListener(utils.nameMap.dragEnd, volumeUp); 203 | this.player.template.volumeButton.classList.add('dplayer-volume-active'); 204 | }); 205 | this.player.template.volumeButtonIcon.addEventListener('click', () => { 206 | if (this.player.video.muted) { 207 | this.player.video.muted = false; 208 | this.player.switchVolumeIcon(); 209 | this.player.bar.set('volume', this.player.volume(), 'width'); 210 | } else { 211 | this.player.video.muted = true; 212 | this.player.template.volumeIcon.innerHTML = Icons.volumeOff; 213 | this.player.bar.set('volume', 0, 'width'); 214 | } 215 | }); 216 | } 217 | 218 | initQualityButton() { 219 | if (this.player.options.video.quality) { 220 | this.player.template.qualityList.addEventListener('click', (e) => { 221 | if (e.target.classList.contains('dplayer-quality-item')) { 222 | this.player.switchQuality(e.target.dataset.index); 223 | } 224 | }); 225 | } 226 | } 227 | 228 | initScreenshotButton() { 229 | if (this.player.options.screenshot) { 230 | this.player.template.camareButton.addEventListener('click', () => { 231 | const canvas = document.createElement('canvas'); 232 | canvas.width = this.player.video.videoWidth; 233 | canvas.height = this.player.video.videoHeight; 234 | canvas.getContext('2d').drawImage(this.player.video, 0, 0, canvas.width, canvas.height); 235 | 236 | let dataURL; 237 | canvas.toBlob((blob) => { 238 | dataURL = URL.createObjectURL(blob); 239 | const link = document.createElement('a'); 240 | link.href = dataURL; 241 | link.download = 'DPlayer.png'; 242 | link.style.display = 'none'; 243 | document.body.appendChild(link); 244 | link.click(); 245 | document.body.removeChild(link); 246 | URL.revokeObjectURL(dataURL); 247 | }); 248 | 249 | this.player.events.trigger('screenshot', dataURL); 250 | }); 251 | } 252 | } 253 | 254 | initAirplayButton() { 255 | if (this.player.options.airplay) { 256 | if (window.WebKitPlaybackTargetAvailabilityEvent) { 257 | this.player.video.addEventListener( 258 | 'webkitplaybacktargetavailabilitychanged', 259 | function (event) { 260 | switch (event.availability) { 261 | case 'available': 262 | this.template.airplayButton.disable = false; 263 | break; 264 | 265 | default: 266 | this.template.airplayButton.disable = true; 267 | } 268 | 269 | this.template.airplayButton.addEventListener( 270 | 'click', 271 | function () { 272 | this.video.webkitShowPlaybackTargetPicker(); 273 | }.bind(this) 274 | ); 275 | }.bind(this.player) 276 | ); 277 | } else { 278 | this.player.template.airplayButton.style.display = 'none'; 279 | } 280 | } 281 | } 282 | 283 | initSubtitleButton() { 284 | if (this.player.options.subtitle) { 285 | this.player.events.on('subtitle_show', () => { 286 | this.player.template.subtitleButton.dataset.balloon = this.player.tran('Hide subtitle'); 287 | this.player.template.subtitleButtonInner.style.opacity = ''; 288 | this.player.user.set('subtitle', 1); 289 | }); 290 | this.player.events.on('subtitle_hide', () => { 291 | this.player.template.subtitleButton.dataset.balloon = this.player.tran('Show subtitle'); 292 | this.player.template.subtitleButtonInner.style.opacity = '0.4'; 293 | this.player.user.set('subtitle', 0); 294 | }); 295 | 296 | this.player.template.subtitleButton.addEventListener('click', () => { 297 | this.player.subtitle.toggle(); 298 | }); 299 | } 300 | } 301 | 302 | setAutoHide() { 303 | this.show(); 304 | clearTimeout(this.autoHideTimer); 305 | this.autoHideTimer = setTimeout(() => { 306 | if (this.player.video.played.length && !this.player.paused && !this.disableAutoHide) { 307 | this.hide(); 308 | } 309 | }, 3000); 310 | } 311 | 312 | show() { 313 | this.player.container.classList.remove('dplayer-hide-controller'); 314 | } 315 | 316 | hide() { 317 | this.player.container.classList.add('dplayer-hide-controller'); 318 | this.player.setting.hide(); 319 | this.player.comment && this.player.comment.hide(); 320 | } 321 | 322 | isShow() { 323 | return !this.player.container.classList.contains('dplayer-hide-controller'); 324 | } 325 | 326 | toggle() { 327 | if (this.isShow()) { 328 | this.hide(); 329 | } else { 330 | this.show(); 331 | } 332 | } 333 | 334 | destroy() { 335 | clearTimeout(this.autoHideTimer); 336 | } 337 | } 338 | 339 | export default Controller; 340 | -------------------------------------------------------------------------------- /src/js/danmaku.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | class Danmaku { 4 | constructor(options) { 5 | this.options = options; 6 | this.container = this.options.container; 7 | this.danTunnel = { 8 | right: {}, 9 | top: {}, 10 | bottom: {}, 11 | }; 12 | this.danIndex = 0; 13 | this.dan = []; 14 | this.showing = true; 15 | this._opacity = this.options.opacity; 16 | this.events = this.options.events; 17 | this.unlimited = this.options.unlimited; 18 | this._measure(''); 19 | 20 | this.load(); 21 | } 22 | 23 | load() { 24 | let apiurl; 25 | if (this.options.api.maximum) { 26 | apiurl = `${this.options.api.address}v3/?id=${this.options.api.id}&max=${this.options.api.maximum}`; 27 | } else { 28 | apiurl = `${this.options.api.address}v3/?id=${this.options.api.id}`; 29 | } 30 | const endpoints = (this.options.api.addition || []).slice(0); 31 | endpoints.push(apiurl); 32 | this.events && this.events.trigger('danmaku_load_start', endpoints); 33 | 34 | this._readAllEndpoints(endpoints, (results) => { 35 | this.dan = [].concat.apply([], results).sort((a, b) => a.time - b.time); 36 | window.requestAnimationFrame(() => { 37 | this.frame(); 38 | }); 39 | 40 | this.options.callback(); 41 | 42 | this.events && this.events.trigger('danmaku_load_end'); 43 | }); 44 | } 45 | 46 | reload(newAPI) { 47 | this.options.api = newAPI; 48 | this.dan = []; 49 | this.clear(); 50 | this.load(); 51 | } 52 | 53 | /** 54 | * Asynchronously read danmaku from all API endpoints 55 | */ 56 | _readAllEndpoints(endpoints, callback) { 57 | const results = []; 58 | let readCount = 0; 59 | 60 | for (let i = 0; i < endpoints.length; ++i) { 61 | this.options.apiBackend.read({ 62 | url: endpoints[i], 63 | success: (data) => { 64 | results[i] = data; 65 | 66 | ++readCount; 67 | if (readCount === endpoints.length) { 68 | callback(results); 69 | } 70 | }, 71 | error: (msg) => { 72 | this.options.error(msg || this.options.tran('Danmaku load failed')); 73 | results[i] = []; 74 | 75 | ++readCount; 76 | if (readCount === endpoints.length) { 77 | callback(results); 78 | } 79 | }, 80 | }); 81 | } 82 | } 83 | 84 | send(dan, callback) { 85 | const danmakuData = { 86 | token: this.options.api.token, 87 | id: this.options.api.id, 88 | author: this.options.api.user, 89 | time: this.options.time(), 90 | text: dan.text, 91 | color: dan.color, 92 | type: dan.type, 93 | }; 94 | this.options.apiBackend.send({ 95 | url: this.options.api.address + 'v3/', 96 | data: danmakuData, 97 | success: callback, 98 | error: (msg) => { 99 | this.options.error(msg || this.options.tran('Danmaku send failed')); 100 | }, 101 | }); 102 | 103 | this.dan.splice(this.danIndex, 0, danmakuData); 104 | this.danIndex++; 105 | const danmaku = { 106 | text: this.htmlEncode(danmakuData.text), 107 | color: danmakuData.color, 108 | type: danmakuData.type, 109 | border: `2px solid ${this.options.borderColor}`, 110 | }; 111 | this.draw(danmaku); 112 | 113 | this.events && this.events.trigger('danmaku_send', danmakuData); 114 | } 115 | 116 | frame() { 117 | if (this.dan.length && !this.paused && this.showing) { 118 | let item = this.dan[this.danIndex]; 119 | const dan = []; 120 | while (item && this.options.time() > parseFloat(item.time)) { 121 | dan.push(item); 122 | item = this.dan[++this.danIndex]; 123 | } 124 | this.draw(dan); 125 | } 126 | window.requestAnimationFrame(() => { 127 | this.frame(); 128 | }); 129 | } 130 | 131 | opacity(percentage) { 132 | if (percentage !== undefined) { 133 | const items = this.container.getElementsByClassName('dplayer-danmaku-item'); 134 | for (let i = 0; i < items.length; i++) { 135 | items[i].style.opacity = percentage; 136 | } 137 | this._opacity = percentage; 138 | 139 | this.events && this.events.trigger('danmaku_opacity', this._opacity); 140 | } 141 | return this._opacity; 142 | } 143 | 144 | /** 145 | * Push a danmaku into DPlayer 146 | * 147 | * @param {Object Array} dan - {text, color, type} 148 | * text - danmaku content 149 | * color - danmaku color, default: `#fff` 150 | * type - danmaku type, `right` `top` `bottom`, default: `right` 151 | */ 152 | draw(dan) { 153 | if (this.showing) { 154 | const itemHeight = this.options.height; 155 | const danWidth = this.container.offsetWidth; 156 | const danHeight = this.container.offsetHeight; 157 | const itemY = parseInt(danHeight / itemHeight); 158 | 159 | const danItemRight = (ele) => { 160 | const eleWidth = ele.offsetWidth || parseInt(ele.style.width); 161 | const eleRight = ele.getBoundingClientRect().right || this.container.getBoundingClientRect().right + eleWidth; 162 | return this.container.getBoundingClientRect().right - eleRight; 163 | }; 164 | 165 | const danSpeed = (width) => (danWidth + width) / 5; 166 | 167 | const getTunnel = (ele, type, width) => { 168 | const tmp = danWidth / danSpeed(width); 169 | 170 | for (let i = 0; this.unlimited || i < itemY; i++) { 171 | const item = this.danTunnel[type][i + '']; 172 | if (item && item.length) { 173 | if (type !== 'right') { 174 | continue; 175 | } 176 | for (let j = 0; j < item.length; j++) { 177 | const danRight = danItemRight(item[j]) - 10; 178 | if (danRight <= danWidth - tmp * danSpeed(parseInt(item[j].style.width)) || danRight <= 0) { 179 | break; 180 | } 181 | if (j === item.length - 1) { 182 | this.danTunnel[type][i + ''].push(ele); 183 | ele.addEventListener('animationend', () => { 184 | this.danTunnel[type][i + ''].splice(0, 1); 185 | }); 186 | return i % itemY; 187 | } 188 | } 189 | } else { 190 | this.danTunnel[type][i + ''] = [ele]; 191 | ele.addEventListener('animationend', () => { 192 | this.danTunnel[type][i + ''].splice(0, 1); 193 | }); 194 | return i % itemY; 195 | } 196 | } 197 | return -1; 198 | }; 199 | 200 | if (Object.prototype.toString.call(dan) !== '[object Array]') { 201 | dan = [dan]; 202 | } 203 | 204 | const docFragment = document.createDocumentFragment(); 205 | 206 | for (let i = 0; i < dan.length; i++) { 207 | dan[i].type = utils.number2Type(dan[i].type); 208 | if (!dan[i].color) { 209 | dan[i].color = 16777215; 210 | } 211 | const item = document.createElement('div'); 212 | item.classList.add('dplayer-danmaku-item'); 213 | item.classList.add(`dplayer-danmaku-${dan[i].type}`); 214 | if (dan[i].border) { 215 | item.innerHTML = `${dan[i].text}`; 216 | } else { 217 | item.innerHTML = dan[i].text; 218 | } 219 | item.style.opacity = this._opacity; 220 | item.style.color = utils.number2Color(dan[i].color); 221 | item.addEventListener('animationend', () => { 222 | this.container.removeChild(item); 223 | }); 224 | 225 | const itemWidth = this._measure(dan[i].text); 226 | let tunnel; 227 | 228 | // adjust 229 | switch (dan[i].type) { 230 | case 'right': 231 | tunnel = getTunnel(item, dan[i].type, itemWidth); 232 | if (tunnel >= 0) { 233 | item.style.width = itemWidth + 1 + 'px'; 234 | item.style.top = itemHeight * tunnel + 'px'; 235 | item.style.transform = `translateX(-${danWidth}px)`; 236 | } 237 | break; 238 | case 'top': 239 | tunnel = getTunnel(item, dan[i].type); 240 | if (tunnel >= 0) { 241 | item.style.top = itemHeight * tunnel + 'px'; 242 | } 243 | break; 244 | case 'bottom': 245 | tunnel = getTunnel(item, dan[i].type); 246 | if (tunnel >= 0) { 247 | item.style.bottom = itemHeight * tunnel + 'px'; 248 | } 249 | break; 250 | default: 251 | console.error(`Can't handled danmaku type: ${dan[i].type}`); 252 | } 253 | 254 | if (tunnel >= 0) { 255 | // move 256 | item.classList.add('dplayer-danmaku-move'); 257 | 258 | // insert 259 | docFragment.appendChild(item); 260 | } 261 | } 262 | 263 | this.container.appendChild(docFragment); 264 | 265 | return docFragment; 266 | } 267 | } 268 | 269 | play() { 270 | this.paused = false; 271 | } 272 | 273 | pause() { 274 | this.paused = true; 275 | } 276 | 277 | _measure(text) { 278 | if (!this.context) { 279 | const measureStyle = getComputedStyle(this.container.getElementsByClassName('dplayer-danmaku-item')[0], null); 280 | this.context = document.createElement('canvas').getContext('2d'); 281 | this.context.font = measureStyle.getPropertyValue('font'); 282 | } 283 | return this.context.measureText(text).width; 284 | } 285 | 286 | seek() { 287 | this.clear(); 288 | for (let i = 0; i < this.dan.length; i++) { 289 | if (this.dan[i].time >= this.options.time()) { 290 | this.danIndex = i; 291 | break; 292 | } 293 | this.danIndex = this.dan.length; 294 | } 295 | } 296 | 297 | clear() { 298 | this.danTunnel = { 299 | right: {}, 300 | top: {}, 301 | bottom: {}, 302 | }; 303 | this.danIndex = 0; 304 | this.options.container.innerHTML = ''; 305 | 306 | this.events && this.events.trigger('danmaku_clear'); 307 | } 308 | 309 | htmlEncode(str) { 310 | return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/'); 311 | } 312 | 313 | resize() { 314 | const danWidth = this.container.offsetWidth; 315 | const items = this.container.getElementsByClassName('dplayer-danmaku-item'); 316 | for (let i = 0; i < items.length; i++) { 317 | items[i].style.transform = `translateX(-${danWidth}px)`; 318 | } 319 | } 320 | 321 | hide() { 322 | this.showing = false; 323 | this.pause(); 324 | this.clear(); 325 | 326 | this.events && this.events.trigger('danmaku_hide'); 327 | } 328 | 329 | show() { 330 | this.seek(); 331 | this.showing = true; 332 | this.play(); 333 | 334 | this.events && this.events.trigger('danmaku_show'); 335 | } 336 | 337 | unlimit(boolean) { 338 | this.unlimited = boolean; 339 | } 340 | } 341 | 342 | export default Danmaku; 343 | -------------------------------------------------------------------------------- /src/js/events.js: -------------------------------------------------------------------------------- 1 | class Events { 2 | constructor() { 3 | this.events = {}; 4 | 5 | this.videoEvents = [ 6 | 'abort', 7 | 'canplay', 8 | 'canplaythrough', 9 | 'durationchange', 10 | 'emptied', 11 | 'ended', 12 | 'error', 13 | 'loadeddata', 14 | 'loadedmetadata', 15 | 'loadstart', 16 | 'mozaudioavailable', 17 | 'pause', 18 | 'play', 19 | 'playing', 20 | 'progress', 21 | 'ratechange', 22 | 'seeked', 23 | 'seeking', 24 | 'stalled', 25 | 'suspend', 26 | 'timeupdate', 27 | 'volumechange', 28 | 'waiting', 29 | ]; 30 | this.playerEvents = [ 31 | 'screenshot', 32 | 'thumbnails_show', 33 | 'thumbnails_hide', 34 | 'danmaku_show', 35 | 'danmaku_hide', 36 | 'danmaku_clear', 37 | 'danmaku_loaded', 38 | 'danmaku_send', 39 | 'danmaku_opacity', 40 | 'contextmenu_show', 41 | 'contextmenu_hide', 42 | 'notice_show', 43 | 'notice_hide', 44 | 'quality_start', 45 | 'quality_end', 46 | 'destroy', 47 | 'resize', 48 | 'fullscreen', 49 | 'fullscreen_cancel', 50 | 'webfullscreen', 51 | 'webfullscreen_cancel', 52 | 'subtitle_show', 53 | 'subtitle_hide', 54 | 'subtitle_change', 55 | 'stats', 56 | 'peerId', 57 | 'peers', 58 | ]; 59 | } 60 | 61 | on(name, callback) { 62 | if (this.type(name) && typeof callback === 'function') { 63 | if (!this.events[name]) { 64 | this.events[name] = []; 65 | } 66 | this.events[name].push(callback); 67 | } 68 | } 69 | 70 | // 为事件注册单次监听器 71 | once(name, callback) { 72 | const wrapFanc = (...args) => { 73 | callback.apply(this. args); 74 | this.off(name, wrapFanc); 75 | }; 76 | this.on(name, wrapFanc); 77 | } 78 | 79 | // 停止监听event事件 80 | off(name, callback) { 81 | const callbacks = this.events[name]; 82 | this.events[name] = callbacks && callbacks.filter((fn) => fn !== callback); 83 | } 84 | 85 | trigger(name, info) { 86 | if (this.events[name] && this.events[name].length) { 87 | for (let i = 0; i < this.events[name].length; i++) { 88 | this.events[name][i](info); 89 | } 90 | } 91 | } 92 | 93 | type(name) { 94 | if (this.playerEvents.indexOf(name) !== -1) { 95 | return 'player'; 96 | } else if (this.videoEvents.indexOf(name) !== -1) { 97 | return 'video'; 98 | } 99 | 100 | console.error(`Unknown event name: ${name}`); 101 | return null; 102 | } 103 | } 104 | 105 | export default Events; 106 | -------------------------------------------------------------------------------- /src/js/fullscreen.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | class FullScreen { 4 | constructor(player) { 5 | this.player = player; 6 | this.lastScrollPosition = { left: 0, top: 0 }; 7 | this.player.events.on('webfullscreen', () => { 8 | this.player.resize(); 9 | }); 10 | this.player.events.on('webfullscreen_cancel', () => { 11 | this.player.resize(); 12 | utils.setScrollPosition(this.lastScrollPosition); 13 | }); 14 | 15 | const fullscreenchange = () => { 16 | this.player.resize(); 17 | if (this.isFullScreen('browser')) { 18 | this.player.events.trigger('fullscreen'); 19 | } else { 20 | utils.setScrollPosition(this.lastScrollPosition); 21 | this.player.events.trigger('fullscreen_cancel'); 22 | } 23 | }; 24 | const docfullscreenchange = () => { 25 | const fullEle = document.fullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; 26 | if (fullEle && fullEle !== this.player.container) { 27 | return; 28 | } 29 | this.player.resize(); 30 | if (fullEle) { 31 | this.player.events.trigger('fullscreen'); 32 | } else { 33 | utils.setScrollPosition(this.lastScrollPosition); 34 | this.player.events.trigger('fullscreen_cancel'); 35 | } 36 | }; 37 | if (/Firefox/.test(navigator.userAgent)) { 38 | document.addEventListener('mozfullscreenchange', docfullscreenchange); 39 | document.addEventListener('fullscreenchange', docfullscreenchange); 40 | } else { 41 | this.player.container.addEventListener('fullscreenchange', fullscreenchange); 42 | this.player.container.addEventListener('webkitfullscreenchange', fullscreenchange); 43 | document.addEventListener('msfullscreenchange', docfullscreenchange); 44 | document.addEventListener('MSFullscreenChange', docfullscreenchange); 45 | } 46 | } 47 | 48 | isFullScreen(type = 'browser') { 49 | switch (type) { 50 | case 'browser': 51 | return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; 52 | case 'web': 53 | return this.player.container.classList.contains('dplayer-fulled'); 54 | } 55 | } 56 | 57 | request(type = 'browser') { 58 | const anotherType = type === 'browser' ? 'web' : 'browser'; 59 | const anotherTypeOn = this.isFullScreen(anotherType); 60 | if (!anotherTypeOn) { 61 | this.lastScrollPosition = utils.getScrollPosition(); 62 | } 63 | 64 | switch (type) { 65 | case 'browser': 66 | if (this.player.container.requestFullscreen) { 67 | this.player.container.requestFullscreen(); 68 | } else if (this.player.container.mozRequestFullScreen) { 69 | this.player.container.mozRequestFullScreen(); 70 | } else if (this.player.container.webkitRequestFullscreen) { 71 | this.player.container.webkitRequestFullscreen(); 72 | } else if (this.player.video.webkitEnterFullscreen) { 73 | // Safari for iOS 74 | this.player.video.webkitEnterFullscreen(); 75 | } else if (this.player.video.webkitEnterFullScreen) { 76 | this.player.video.webkitEnterFullScreen(); 77 | } else if (this.player.container.msRequestFullscreen) { 78 | this.player.container.msRequestFullscreen(); 79 | } 80 | break; 81 | case 'web': 82 | this.player.container.classList.add('dplayer-fulled'); 83 | document.body.classList.add('dplayer-web-fullscreen-fix'); 84 | this.player.events.trigger('webfullscreen'); 85 | break; 86 | } 87 | 88 | if (anotherTypeOn) { 89 | this.cancel(anotherType); 90 | } 91 | } 92 | 93 | cancel(type = 'browser') { 94 | switch (type) { 95 | case 'browser': 96 | if (document.cancelFullScreen) { 97 | document.cancelFullScreen(); 98 | } else if (document.mozCancelFullScreen) { 99 | document.mozCancelFullScreen(); 100 | } else if (document.webkitCancelFullScreen) { 101 | document.webkitCancelFullScreen(); 102 | } else if (document.webkitCancelFullscreen) { 103 | document.webkitCancelFullscreen(); 104 | } else if (document.msCancelFullScreen) { 105 | document.msCancelFullScreen(); 106 | } else if (document.msExitFullscreen) { 107 | document.msExitFullscreen(); 108 | } 109 | break; 110 | case 'web': 111 | this.player.container.classList.remove('dplayer-fulled'); 112 | document.body.classList.remove('dplayer-web-fullscreen-fix'); 113 | this.player.events.trigger('webfullscreen_cancel'); 114 | break; 115 | } 116 | } 117 | 118 | toggle(type = 'browser') { 119 | if (this.isFullScreen(type)) { 120 | this.cancel(type); 121 | } else { 122 | this.request(type); 123 | } 124 | } 125 | } 126 | 127 | export default FullScreen; 128 | -------------------------------------------------------------------------------- /src/js/hotkey.js: -------------------------------------------------------------------------------- 1 | class HotKey { 2 | constructor(player) { 3 | if (player.options.hotkey) { 4 | document.addEventListener('keydown', (e) => { 5 | if (player.focus) { 6 | const tag = document.activeElement.tagName.toUpperCase(); 7 | const editable = document.activeElement.getAttribute('contenteditable'); 8 | if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') { 9 | const event = e || window.event; 10 | let percentage; 11 | switch (event.keyCode) { 12 | case 32: 13 | event.preventDefault(); 14 | player.toggle(); 15 | break; 16 | case 37: 17 | event.preventDefault(); 18 | if (player.options.live) { 19 | break; 20 | } 21 | player.seek(player.video.currentTime - 5); 22 | player.controller.setAutoHide(); 23 | break; 24 | case 39: 25 | event.preventDefault(); 26 | if (player.options.live) { 27 | break; 28 | } 29 | player.seek(player.video.currentTime + 5); 30 | player.controller.setAutoHide(); 31 | break; 32 | case 38: 33 | event.preventDefault(); 34 | percentage = player.volume() + 0.1; 35 | player.volume(percentage); 36 | break; 37 | case 40: 38 | event.preventDefault(); 39 | percentage = player.volume() - 0.1; 40 | player.volume(percentage); 41 | break; 42 | } 43 | } 44 | } 45 | }); 46 | } 47 | 48 | document.addEventListener('keydown', (e) => { 49 | const event = e || window.event; 50 | switch (event.keyCode) { 51 | case 27: 52 | if (player.fullScreen.isFullScreen('web')) { 53 | player.fullScreen.cancel('web'); 54 | } 55 | break; 56 | } 57 | }); 58 | } 59 | } 60 | 61 | export default HotKey; 62 | -------------------------------------------------------------------------------- /src/js/i18n.js: -------------------------------------------------------------------------------- 1 | /* 2 | W3C def language codes is : 3 | language-code = primary-code ( "-" subcode ) 4 | primary-code ISO 639-1 ( the names of language with 2 code ) 5 | subcode ISO 3166 ( the names of countries ) 6 | 7 | NOTE: use lowercase to prevent case typo from user! 8 | Use this as shown below..... */ 9 | 10 | function i18n(lang) { 11 | this.lang = lang; 12 | this.tran = (text) => { 13 | if (tranTxt[this.lang] && tranTxt[this.lang][text]) { 14 | return tranTxt[this.lang][text]; 15 | } else { 16 | return text; 17 | } 18 | }; 19 | } 20 | 21 | // add translation text here 22 | const tranTxt = { 23 | 'zh-cn': { 24 | 'Danmaku is loading': '弹幕加载中', 25 | Top: '顶部', 26 | Bottom: '底部', 27 | Rolling: '滚动', 28 | 'Input danmaku, hit Enter': '输入弹幕,回车发送', 29 | 'About author': '关于作者', 30 | 'DPlayer feedback': '播放器意见反馈', 31 | 'About DPlayer': '关于 DPlayer 播放器', 32 | Loop: '洗脑循环', 33 | Speed: '速度', 34 | 'Opacity for danmaku': '弹幕透明度', 35 | Normal: '正常', 36 | 'Please input danmaku content!': '要输入弹幕内容啊喂!', 37 | 'Set danmaku color': '设置弹幕颜色', 38 | 'Set danmaku type': '设置弹幕类型', 39 | 'Show danmaku': '显示弹幕', 40 | 'Video load failed': '视频加载失败', 41 | 'Danmaku load failed': '弹幕加载失败', 42 | 'Danmaku send failed': '弹幕发送失败', 43 | 'Switching to': '正在切换至', 44 | 'Switched to': '已经切换至', 45 | quality: '画质', 46 | FF: '快进', 47 | REW: '快退', 48 | 'Unlimited danmaku': '海量弹幕', 49 | 'Send danmaku': '发送弹幕', 50 | Setting: '设置', 51 | 'Full screen': '全屏', 52 | 'Web full screen': '页面全屏', 53 | Send: '发送', 54 | Screenshot: '截图', 55 | AirPlay: '无线投屏', 56 | s: '秒', 57 | 'Show subtitle': '显示字幕', 58 | 'Hide subtitle': '隐藏字幕', 59 | Volume: '音量', 60 | Live: '直播', 61 | 'Video info': '视频统计信息', 62 | }, 63 | 'zh-tw': { 64 | 'Danmaku is loading': '彈幕載入中', 65 | Top: '頂部', 66 | Bottom: '底部', 67 | Rolling: '滾動', 68 | 'Input danmaku, hit Enter': '輸入彈幕,Enter 發送', 69 | 'About author': '關於作者', 70 | 'DPlayer feedback': '播放器意見回饋', 71 | 'About DPlayer': '關於 DPlayer 播放器', 72 | Loop: '循環播放', 73 | Speed: '速度', 74 | 'Opacity for danmaku': '彈幕透明度', 75 | Normal: '正常', 76 | 'Please input danmaku content!': '請輸入彈幕內容啊!', 77 | 'Set danmaku color': '設定彈幕顏色', 78 | 'Set danmaku type': '設定彈幕類型', 79 | 'Show danmaku': '顯示彈幕', 80 | 'Video load failed': '影片載入失敗', 81 | 'Danmaku load failed': '彈幕載入失敗', 82 | 'Danmaku send failed': '彈幕發送失敗', 83 | 'Switching to': '正在切換至', 84 | 'Switched to': '已經切換至', 85 | quality: '畫質', 86 | FF: '快進', 87 | REW: '快退', 88 | 'Unlimited danmaku': '巨量彈幕', 89 | 'Send danmaku': '發送彈幕', 90 | Setting: '設定', 91 | 'Full screen': '全螢幕', 92 | 'Web full screen': '頁面全螢幕', 93 | Send: '發送', 94 | Screenshot: '截圖', 95 | AirPlay: '無線投屏', 96 | s: '秒', 97 | 'Show subtitle': '顯示字幕', 98 | 'Hide subtitle': '隱藏字幕', 99 | Volume: '音量', 100 | Live: '直播', 101 | 'Video info': '影片&P2P統計訊息', 102 | }, 103 | }; 104 | 105 | export default i18n; 106 | -------------------------------------------------------------------------------- /src/js/icons.js: -------------------------------------------------------------------------------- 1 | import play from '../assets/play.svg'; 2 | import pause from '../assets/pause.svg'; 3 | import volumeUp from '../assets/volume-up.svg'; 4 | import volumeDown from '../assets/volume-down.svg'; 5 | import volumeOff from '../assets/volume-off.svg'; 6 | import full from '../assets/full.svg'; 7 | import fullWeb from '../assets/full-web.svg'; 8 | import setting from '../assets/setting.svg'; 9 | import right from '../assets/right.svg'; 10 | import comment from '../assets/comment.svg'; 11 | import commentOff from '../assets/comment-off.svg'; 12 | import send from '../assets/send.svg'; 13 | import pallette from '../assets/pallette.svg'; 14 | import camera from '../assets/camera.svg'; 15 | import airplay from '../assets/airplay.svg'; 16 | import subtitle from '../assets/subtitle.svg'; 17 | import loading from '../assets/loading.svg'; 18 | 19 | const Icons = { 20 | play: play, 21 | pause: pause, 22 | volumeUp: volumeUp, 23 | volumeDown: volumeDown, 24 | volumeOff: volumeOff, 25 | full: full, 26 | fullWeb: fullWeb, 27 | setting: setting, 28 | right: right, 29 | comment: comment, 30 | commentOff: commentOff, 31 | send: send, 32 | pallette: pallette, 33 | camera: camera, 34 | subtitle: subtitle, 35 | loading: loading, 36 | airplay: airplay, 37 | }; 38 | 39 | export default Icons; 40 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import '../css/index.scss'; 2 | import DPlayer from './player'; 3 | 4 | /* global DPLAYER_VERSION GIT_HASH */ 5 | // console.log(`${'\n'} %c DPlayer v${DPLAYER_VERSION} ${GIT_HASH} %c http://dplayer.js.org ${'\n'}${'\n'}`, 'color: #fadfa3; background: #030307; padding:5px 0;', 'background: #fadfa3; padding:5px 0;'); 6 | 7 | export default DPlayer; 8 | -------------------------------------------------------------------------------- /src/js/info-panel.js: -------------------------------------------------------------------------------- 1 | /* global DPLAYER_VERSION GIT_HASH */ 2 | 3 | class InfoPanel { 4 | constructor(player) { 5 | this.container = player.template.infoPanel; 6 | this.template = player.template; 7 | this.video = player.video; 8 | this.player = player; 9 | 10 | this.template.infoPanelClose.addEventListener('click', () => { 11 | this.hide(); 12 | }); 13 | } 14 | 15 | show() { 16 | this.beginTime = Date.now(); 17 | this.update(); 18 | this.player.timer.enable('info'); 19 | this.player.timer.enable('fps'); 20 | this.container.classList.remove('dplayer-info-panel-hide'); 21 | } 22 | 23 | hide() { 24 | this.player.timer.disable('info'); 25 | this.player.timer.disable('fps'); 26 | this.container.classList.add('dplayer-info-panel-hide'); 27 | } 28 | 29 | triggle() { 30 | if (this.container.classList.contains('dplayer-info-panel-hide')) { 31 | this.show(); 32 | } else { 33 | this.hide(); 34 | } 35 | } 36 | 37 | update() { 38 | this.template.infoVersion.innerHTML = `v${DPLAYER_VERSION}`; 39 | this.template.infoType.innerHTML = this.player.type; 40 | this.template.infoUrl.innerHTML = this.player.options.video.url; 41 | this.template.infoResolution.innerHTML = `${this.player.video.videoWidth} x ${this.player.video.videoHeight}`; 42 | this.template.infoDuration.innerHTML = this.player.video.duration; 43 | if (this.player.options.danmaku) { 44 | this.template.infoDanmakuId.innerHTML = this.player.options.danmaku.id; 45 | this.template.infoDanmakuApi.innerHTML = this.player.options.danmaku.api; 46 | this.template.infoDanmakuAmount.innerHTML = this.player.danmaku.dan.length; 47 | } 48 | // P2P Info 49 | const p2pInfo = this.player.p2pInfo; 50 | // console.warn(`${p2pInfo.p2pDownloaded} ${p2pInfo.httpDownloaded}`) 51 | this.template.infoP2pVersion.innerHTML = `v${p2pInfo.version}`; 52 | this.template.infoP2pDownloaded.innerHTML = `${(p2pInfo.p2pDownloaded / 1024).toFixed(2)}MB`; 53 | this.template.infoP2pRatio.innerHTML = `${Math.round(p2pInfo.p2pDownloaded / (p2pInfo.p2pDownloaded + p2pInfo.httpDownloaded) * 100).toFixed(0)}%`; 54 | this.template.infoP2pUploaded.innerHTML = `${(p2pInfo.uploaded / 1024).toFixed(2)}MB`; 55 | this.template.infoPeerid.innerHTML = `${p2pInfo.peerId}`; 56 | this.template.infoPeers.innerHTML = `${p2pInfo.peers}`; 57 | this.template.infoDecoder.innerHTML = `${p2pInfo.decoder}`; 58 | } 59 | 60 | fps(value) { 61 | this.template.infoFPS.innerHTML = `${value.toFixed(1)}`; 62 | } 63 | } 64 | 65 | export default InfoPanel; 66 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | /* global DPLAYER_VERSION */ 2 | import defaultApiBackend from './api.js'; 3 | 4 | export default (options) => { 5 | // default options 6 | const defaultOption = { 7 | container: options.element || document.getElementsByClassName('dplayer')[0], 8 | live: false, 9 | autoplay: false, 10 | theme: '#b7daff', 11 | loop: false, 12 | lang: (navigator.language || navigator.browserLanguage).toLowerCase(), 13 | screenshot: false, 14 | airplay: true, 15 | hotkey: true, 16 | preload: 'metadata', 17 | volume: 0.7, 18 | playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2], 19 | apiBackend: defaultApiBackend, 20 | video: {}, 21 | contextmenu: [], 22 | mutex: true, 23 | pluginOptions: { hls: {}, flv: {}, dash: {}, webtorrent: {} }, 24 | }; 25 | for (const defaultKey in defaultOption) { 26 | if (defaultOption.hasOwnProperty(defaultKey) && !options.hasOwnProperty(defaultKey)) { 27 | options[defaultKey] = defaultOption[defaultKey]; 28 | } 29 | } 30 | if (options.video) { 31 | !options.video.type && (options.video.type = 'auto'); 32 | } 33 | if (typeof options.danmaku === 'object' && options.danmaku) { 34 | !options.danmaku.user && (options.danmaku.user = 'DIYgod'); 35 | } 36 | if (options.subtitle) { 37 | !options.subtitle.type && (options.subtitle.type = 'webvtt'); 38 | !options.subtitle.fontSize && (options.subtitle.fontSize = '20px'); 39 | !options.subtitle.bottom && (options.subtitle.bottom = '40px'); 40 | !options.subtitle.color && (options.subtitle.color = '#fff'); 41 | } 42 | 43 | if (options.video.quality) { 44 | options.video.url = options.video.quality[options.video.defaultQuality].url; 45 | } 46 | 47 | if (options.lang) { 48 | options.lang = options.lang.toLowerCase(); 49 | } 50 | 51 | options.contextmenu = options.contextmenu.concat([ 52 | { 53 | text: 'Video info', 54 | click: (player) => { 55 | player.infoPanel.triggle(); 56 | }, 57 | }, 58 | // { 59 | // text: 'About author', 60 | // link: 'https://diygod.me', 61 | // }, 62 | // { 63 | // text: `DPlayer v${DPLAYER_VERSION}`, 64 | // link: 'https://github.com/MoePlayer/DPlayer', 65 | // }, 66 | ]); 67 | 68 | return options; 69 | }; 70 | -------------------------------------------------------------------------------- /src/js/play-state.js: -------------------------------------------------------------------------------- 1 | const prefix = 'Vod'; 2 | 3 | class PlayState { 4 | constructor(video, url) { 5 | this.video = video; 6 | 7 | this.url = url; 8 | 9 | this.key = prefix + this.url + ' '; 10 | } 11 | 12 | startRecord() { 13 | this.timer = window.setInterval(() => { 14 | // console.warn(`set ${this.key} ${this.video.currentTime}`); 15 | this.set(this.key, this.video.currentTime); 16 | }, 1000); 17 | } 18 | 19 | getLastState() { 20 | const lastTime = this.get(this.key); 21 | return lastTime; 22 | } 23 | 24 | destroy() { 25 | if (this.timer) { 26 | clearInterval(this.timer); 27 | this.timer = null; 28 | } 29 | } 30 | 31 | set(key, val) { 32 | window.sessionStorage.setItem(key, val); 33 | } 34 | 35 | get(key) { 36 | return window.sessionStorage.getItem(key); 37 | } 38 | 39 | del(key) { 40 | window.sessionStorage.removeItem(key); 41 | } 42 | 43 | clear() { 44 | window.sessionStorage.clear(); 45 | } 46 | } 47 | 48 | export default PlayState; 49 | -------------------------------------------------------------------------------- /src/js/setting.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | class Setting { 4 | constructor(player) { 5 | this.player = player; 6 | 7 | this.player.template.mask.addEventListener('click', () => { 8 | this.hide(); 9 | }); 10 | this.player.template.settingButton.addEventListener('click', () => { 11 | this.show(); 12 | }); 13 | 14 | // loop 15 | this.loop = this.player.options.loop; 16 | this.player.template.loopToggle.checked = this.loop; 17 | this.player.template.loop.addEventListener('click', () => { 18 | this.player.template.loopToggle.checked = !this.player.template.loopToggle.checked; 19 | if (this.player.template.loopToggle.checked) { 20 | this.loop = true; 21 | } else { 22 | this.loop = false; 23 | } 24 | this.hide(); 25 | }); 26 | 27 | // show danmaku 28 | this.showDanmaku = this.player.user.get('danmaku'); 29 | if (!this.showDanmaku) { 30 | this.player.danmaku && this.player.danmaku.hide(); 31 | } 32 | this.player.template.showDanmakuToggle.checked = this.showDanmaku; 33 | this.player.template.showDanmaku.addEventListener('click', () => { 34 | this.player.template.showDanmakuToggle.checked = !this.player.template.showDanmakuToggle.checked; 35 | if (this.player.template.showDanmakuToggle.checked) { 36 | this.showDanmaku = true; 37 | this.player.danmaku.show(); 38 | } else { 39 | this.showDanmaku = false; 40 | this.player.danmaku.hide(); 41 | } 42 | this.player.user.set('danmaku', this.showDanmaku ? 1 : 0); 43 | this.hide(); 44 | }); 45 | 46 | // unlimit danmaku 47 | this.unlimitDanmaku = this.player.user.get('unlimited'); 48 | this.player.template.unlimitDanmakuToggle.checked = this.unlimitDanmaku; 49 | this.player.template.unlimitDanmaku.addEventListener('click', () => { 50 | this.player.template.unlimitDanmakuToggle.checked = !this.player.template.unlimitDanmakuToggle.checked; 51 | if (this.player.template.unlimitDanmakuToggle.checked) { 52 | this.unlimitDanmaku = true; 53 | this.player.danmaku.unlimit(true); 54 | } else { 55 | this.unlimitDanmaku = false; 56 | this.player.danmaku.unlimit(false); 57 | } 58 | this.player.user.set('unlimited', this.unlimitDanmaku ? 1 : 0); 59 | this.hide(); 60 | }); 61 | 62 | // speed 63 | this.player.template.speed.addEventListener('click', () => { 64 | this.player.template.settingBox.classList.add('dplayer-setting-box-narrow'); 65 | this.player.template.settingBox.classList.add('dplayer-setting-box-speed'); 66 | }); 67 | for (let i = 0; i < this.player.template.speedItem.length; i++) { 68 | this.player.template.speedItem[i].addEventListener('click', () => { 69 | this.player.speed(this.player.template.speedItem[i].dataset.speed); 70 | this.hide(); 71 | }); 72 | } 73 | 74 | // danmaku opacity 75 | if (this.player.danmaku) { 76 | const dWidth = 130; 77 | this.player.on('danmaku_opacity', (percentage) => { 78 | this.player.bar.set('danmaku', percentage, 'width'); 79 | this.player.user.set('opacity', percentage); 80 | }); 81 | this.player.danmaku.opacity(this.player.user.get('opacity')); 82 | 83 | const danmakuMove = (event) => { 84 | const e = event || window.event; 85 | let percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.danmakuOpacityBarWrap)) / dWidth; 86 | percentage = Math.max(percentage, 0); 87 | percentage = Math.min(percentage, 1); 88 | this.player.danmaku.opacity(percentage); 89 | }; 90 | const danmakuUp = () => { 91 | document.removeEventListener(utils.nameMap.dragEnd, danmakuUp); 92 | document.removeEventListener(utils.nameMap.dragMove, danmakuMove); 93 | this.player.template.danmakuOpacityBox.classList.remove('dplayer-setting-danmaku-active'); 94 | }; 95 | 96 | this.player.template.danmakuOpacityBarWrapWrap.addEventListener('click', (event) => { 97 | const e = event || window.event; 98 | let percentage = ((e.clientX || e.changedTouches[0].clientX) - utils.getBoundingClientRectViewLeft(this.player.template.danmakuOpacityBarWrap)) / dWidth; 99 | percentage = Math.max(percentage, 0); 100 | percentage = Math.min(percentage, 1); 101 | this.player.danmaku.opacity(percentage); 102 | }); 103 | this.player.template.danmakuOpacityBarWrapWrap.addEventListener(utils.nameMap.dragStart, () => { 104 | document.addEventListener(utils.nameMap.dragMove, danmakuMove); 105 | document.addEventListener(utils.nameMap.dragEnd, danmakuUp); 106 | this.player.template.danmakuOpacityBox.classList.add('dplayer-setting-danmaku-active'); 107 | }); 108 | } 109 | } 110 | 111 | hide() { 112 | this.player.template.settingBox.classList.remove('dplayer-setting-box-open'); 113 | this.player.template.mask.classList.remove('dplayer-mask-show'); 114 | setTimeout(() => { 115 | this.player.template.settingBox.classList.remove('dplayer-setting-box-narrow'); 116 | this.player.template.settingBox.classList.remove('dplayer-setting-box-speed'); 117 | }, 300); 118 | 119 | this.player.controller.disableAutoHide = false; 120 | } 121 | 122 | show() { 123 | this.player.template.settingBox.classList.add('dplayer-setting-box-open'); 124 | this.player.template.mask.classList.add('dplayer-mask-show'); 125 | 126 | this.player.controller.disableAutoHide = true; 127 | } 128 | } 129 | 130 | export default Setting; 131 | -------------------------------------------------------------------------------- /src/js/subtitle.js: -------------------------------------------------------------------------------- 1 | class Subtitle { 2 | constructor(container, video, options, events) { 3 | this.container = container; 4 | this.video = video; 5 | this.options = options; 6 | this.events = events; 7 | 8 | this.init(); 9 | } 10 | 11 | init() { 12 | this.container.style.fontSize = this.options.fontSize; 13 | this.container.style.bottom = this.options.bottom; 14 | this.container.style.color = this.options.color; 15 | 16 | if (this.video.textTracks && this.video.textTracks[0]) { 17 | const track = this.video.textTracks[0]; 18 | 19 | track.oncuechange = () => { 20 | const cue = track.activeCues[0]; 21 | this.container.innerHTML = ''; 22 | if (cue) { 23 | const template = document.createElement('div'); 24 | template.appendChild(cue.getCueAsHTML()); 25 | const trackHtml = template.innerHTML 26 | .split(/\r?\n/) 27 | .map((item) => `

${item}

`) 28 | .join(''); 29 | this.container.innerHTML = trackHtml; 30 | } 31 | this.events.trigger('subtitle_change'); 32 | }; 33 | } 34 | } 35 | 36 | show() { 37 | this.container.classList.remove('dplayer-subtitle-hide'); 38 | this.events.trigger('subtitle_show'); 39 | } 40 | 41 | hide() { 42 | this.container.classList.add('dplayer-subtitle-hide'); 43 | this.events.trigger('subtitle_hide'); 44 | } 45 | 46 | toggle() { 47 | if (this.container.classList.contains('dplayer-subtitle-hide')) { 48 | this.show(); 49 | } else { 50 | this.hide(); 51 | } 52 | } 53 | } 54 | 55 | export default Subtitle; 56 | -------------------------------------------------------------------------------- /src/js/template.js: -------------------------------------------------------------------------------- 1 | import Icons from './icons'; 2 | import tplPlayer from '../template/player.art'; 3 | import utils from './utils'; 4 | 5 | class Template { 6 | constructor(options) { 7 | this.container = options.container; 8 | this.options = options.options; 9 | this.index = options.index; 10 | this.tran = options.tran; 11 | this.init(); 12 | } 13 | 14 | init() { 15 | this.container.innerHTML = tplPlayer({ 16 | options: this.options, 17 | index: this.index, 18 | tran: this.tran, 19 | icons: Icons, 20 | mobile: utils.isMobile, 21 | video: { 22 | current: true, 23 | pic: this.options.video.pic, 24 | screenshot: this.options.screenshot, 25 | airplay: this.options.airplay, 26 | preload: this.options.preload, 27 | url: this.options.video.url, 28 | subtitle: this.options.subtitle, 29 | }, 30 | }); 31 | 32 | this.volumeBar = this.container.querySelector('.dplayer-volume-bar-inner'); 33 | this.volumeBarWrap = this.container.querySelector('.dplayer-volume-bar'); 34 | this.volumeBarWrapWrap = this.container.querySelector('.dplayer-volume-bar-wrap'); 35 | this.volumeButton = this.container.querySelector('.dplayer-volume'); 36 | this.volumeButtonIcon = this.container.querySelector('.dplayer-volume-icon'); 37 | this.volumeIcon = this.container.querySelector('.dplayer-volume-icon .dplayer-icon-content'); 38 | this.playedBar = this.container.querySelector('.dplayer-played'); 39 | this.loadedBar = this.container.querySelector('.dplayer-loaded'); 40 | this.playedBarWrap = this.container.querySelector('.dplayer-bar-wrap'); 41 | this.playedBarTime = this.container.querySelector('.dplayer-bar-time'); 42 | this.danmaku = this.container.querySelector('.dplayer-danmaku'); 43 | this.danmakuLoading = this.container.querySelector('.dplayer-danloading'); 44 | this.video = this.container.querySelector('.dplayer-video-current'); 45 | this.bezel = this.container.querySelector('.dplayer-bezel-icon'); 46 | this.playButton = this.container.querySelector('.dplayer-play-icon'); 47 | this.mobilePlayButton = this.container.querySelector('.dplayer-mobile-play'); 48 | this.videoWrap = this.container.querySelector('.dplayer-video-wrap'); 49 | this.controllerMask = this.container.querySelector('.dplayer-controller-mask'); 50 | this.ptime = this.container.querySelector('.dplayer-ptime'); 51 | this.settingButton = this.container.querySelector('.dplayer-setting-icon'); 52 | this.settingBox = this.container.querySelector('.dplayer-setting-box'); 53 | this.mask = this.container.querySelector('.dplayer-mask'); 54 | this.loop = this.container.querySelector('.dplayer-setting-loop'); 55 | this.loopToggle = this.container.querySelector('.dplayer-setting-loop .dplayer-toggle-setting-input'); 56 | this.showDanmaku = this.container.querySelector('.dplayer-setting-showdan'); 57 | this.showDanmakuToggle = this.container.querySelector('.dplayer-showdan-setting-input'); 58 | this.unlimitDanmaku = this.container.querySelector('.dplayer-setting-danunlimit'); 59 | this.unlimitDanmakuToggle = this.container.querySelector('.dplayer-danunlimit-setting-input'); 60 | this.speed = this.container.querySelector('.dplayer-setting-speed'); 61 | this.speedItem = this.container.querySelectorAll('.dplayer-setting-speed-item'); 62 | this.danmakuOpacityBar = this.container.querySelector('.dplayer-danmaku-bar-inner'); 63 | this.danmakuOpacityBarWrap = this.container.querySelector('.dplayer-danmaku-bar'); 64 | this.danmakuOpacityBarWrapWrap = this.container.querySelector('.dplayer-danmaku-bar-wrap'); 65 | this.danmakuOpacityBox = this.container.querySelector('.dplayer-setting-danmaku'); 66 | this.dtime = this.container.querySelector('.dplayer-dtime'); 67 | this.controller = this.container.querySelector('.dplayer-controller'); 68 | this.commentInput = this.container.querySelector('.dplayer-comment-input'); 69 | this.commentButton = this.container.querySelector('.dplayer-comment-icon'); 70 | this.commentSettingBox = this.container.querySelector('.dplayer-comment-setting-box'); 71 | this.commentSettingButton = this.container.querySelector('.dplayer-comment-setting-icon'); 72 | this.commentSettingFill = this.container.querySelector('.dplayer-comment-setting-icon path'); 73 | this.commentSendButton = this.container.querySelector('.dplayer-send-icon'); 74 | this.commentSendFill = this.container.querySelector('.dplayer-send-icon path'); 75 | this.commentColorSettingBox = this.container.querySelector('.dplayer-comment-setting-color'); 76 | this.browserFullButton = this.container.querySelector('.dplayer-full-icon'); 77 | this.webFullButton = this.container.querySelector('.dplayer-full-in-icon'); 78 | this.menu = this.container.querySelector('.dplayer-menu'); 79 | this.menuItem = this.container.querySelectorAll('.dplayer-menu-item'); 80 | this.qualityList = this.container.querySelector('.dplayer-quality-list'); 81 | this.camareButton = this.container.querySelector('.dplayer-camera-icon'); 82 | this.airplayButton = this.container.querySelector('.dplayer-airplay-icon'); 83 | this.subtitleButton = this.container.querySelector('.dplayer-subtitle-icon'); 84 | this.subtitleButtonInner = this.container.querySelector('.dplayer-subtitle-icon .dplayer-icon-content'); 85 | this.subtitle = this.container.querySelector('.dplayer-subtitle'); 86 | this.qualityButton = this.container.querySelector('.dplayer-quality-icon'); 87 | this.barPreview = this.container.querySelector('.dplayer-bar-preview'); 88 | this.barWrap = this.container.querySelector('.dplayer-bar-wrap'); 89 | this.notice = this.container.querySelector('.dplayer-notice'); 90 | this.infoPanel = this.container.querySelector('.dplayer-info-panel'); 91 | this.infoPanelClose = this.container.querySelector('.dplayer-info-panel-close'); 92 | this.infoVersion = this.container.querySelector('.dplayer-info-panel-item-version .dplayer-info-panel-item-data'); 93 | this.infoFPS = this.container.querySelector('.dplayer-info-panel-item-fps .dplayer-info-panel-item-data'); 94 | this.infoType = this.container.querySelector('.dplayer-info-panel-item-type .dplayer-info-panel-item-data'); 95 | this.infoUrl = this.container.querySelector('.dplayer-info-panel-item-url .dplayer-info-panel-item-data'); 96 | this.infoResolution = this.container.querySelector('.dplayer-info-panel-item-resolution .dplayer-info-panel-item-data'); 97 | this.infoDuration = this.container.querySelector('.dplayer-info-panel-item-duration .dplayer-info-panel-item-data'); 98 | this.infoDanmakuId = this.container.querySelector('.dplayer-info-panel-item-danmaku-id .dplayer-info-panel-item-data'); 99 | this.infoDanmakuApi = this.container.querySelector('.dplayer-info-panel-item-danmaku-api .dplayer-info-panel-item-data'); 100 | this.infoDanmakuAmount = this.container.querySelector('.dplayer-info-panel-item-danmaku-amount .dplayer-info-panel-item-data'); 101 | // P2P Info 102 | this.infoP2pVersion = this.container.querySelector('.dplayer-info-panel-item-p2p-version .dplayer-info-panel-item-data'); 103 | this.infoP2pDownloaded = this.container.querySelector('.dplayer-info-panel-item-p2p-downloaded .dplayer-info-panel-item-data'); 104 | this.infoP2pRatio = this.container.querySelector('.dplayer-info-panel-item-p2p-ratio .dplayer-info-panel-item-data'); 105 | this.infoP2pUploaded = this.container.querySelector('.dplayer-info-panel-item-p2p-uploaded .dplayer-info-panel-item-data'); 106 | this.infoPeers = this.container.querySelector('.dplayer-info-panel-item-peers .dplayer-info-panel-item-data'); 107 | this.infoPeerid = this.container.querySelector('.dplayer-info-panel-item-peerid .dplayer-info-panel-item-data'); 108 | this.infoDecoder = this.container.querySelector('.dplayer-info-panel-item-decoder .dplayer-info-panel-item-data'); 109 | } 110 | } 111 | 112 | export default Template; 113 | -------------------------------------------------------------------------------- /src/js/thumbnails.js: -------------------------------------------------------------------------------- 1 | class Thumbnails { 2 | constructor(options) { 3 | this.container = options.container; 4 | this.barWidth = options.barWidth; 5 | this.container.style.backgroundImage = `url('${options.url}')`; 6 | this.events = options.events; 7 | } 8 | 9 | resize(width, height, barWrapWidth) { 10 | this.container.style.width = `${width}px`; 11 | this.container.style.height = `${height}px`; 12 | this.container.style.top = `${-height + 2}px`; 13 | this.barWidth = barWrapWidth; 14 | } 15 | 16 | show() { 17 | this.container.style.display = 'block'; 18 | this.events && this.events.trigger('thumbnails_show'); 19 | } 20 | 21 | move(position) { 22 | this.container.style.backgroundPosition = `-${(Math.ceil((position / this.barWidth) * 100) - 1) * 160}px 0`; 23 | this.container.style.left = `${Math.min(Math.max(position - this.container.offsetWidth / 2, -10), this.barWidth - 150)}px`; 24 | } 25 | 26 | hide() { 27 | this.container.style.display = 'none'; 28 | 29 | this.events && this.events.trigger('thumbnails_hide'); 30 | } 31 | } 32 | 33 | export default Thumbnails; 34 | -------------------------------------------------------------------------------- /src/js/timer.js: -------------------------------------------------------------------------------- 1 | class Timer { 2 | constructor(player) { 3 | this.player = player; 4 | 5 | window.requestAnimationFrame = (() => 6 | window.requestAnimationFrame || 7 | window.webkitRequestAnimationFrame || 8 | window.mozRequestAnimationFrame || 9 | window.oRequestAnimationFrame || 10 | window.msRequestAnimationFrame || 11 | function (callback) { 12 | window.setTimeout(callback, 1000 / 60); 13 | })(); 14 | 15 | this.types = ['loading', 'info', 'fps']; 16 | 17 | this.init(); 18 | } 19 | 20 | init() { 21 | this.types.map((item) => { 22 | if (item !== 'fps') { 23 | this[`init${item}Checker`](); 24 | } 25 | return item; 26 | }); 27 | } 28 | 29 | initloadingChecker() { 30 | let lastPlayPos = 0; 31 | let currentPlayPos = 0; 32 | let bufferingDetected = false; 33 | this.loadingChecker = setInterval(() => { 34 | if (this.enableloadingChecker) { 35 | // whether the video is buffering 36 | currentPlayPos = this.player.video.currentTime; 37 | if (!bufferingDetected && currentPlayPos === lastPlayPos && !this.player.video.paused) { 38 | this.player.container.classList.add('dplayer-loading'); 39 | bufferingDetected = true; 40 | } 41 | if (bufferingDetected && currentPlayPos > lastPlayPos && !this.player.video.paused) { 42 | this.player.container.classList.remove('dplayer-loading'); 43 | bufferingDetected = false; 44 | } 45 | lastPlayPos = currentPlayPos; 46 | } 47 | }, 100); 48 | } 49 | 50 | initfpsChecker() { 51 | window.requestAnimationFrame(() => { 52 | if (this.enablefpsChecker) { 53 | this.initfpsChecker(); 54 | if (!this.fpsStart) { 55 | this.fpsStart = new Date(); 56 | this.fpsIndex = 0; 57 | } else { 58 | this.fpsIndex++; 59 | const fpsCurrent = new Date(); 60 | if (fpsCurrent - this.fpsStart > 1000) { 61 | this.player.infoPanel.fps((this.fpsIndex / (fpsCurrent - this.fpsStart)) * 1000); 62 | this.fpsStart = new Date(); 63 | this.fpsIndex = 0; 64 | } 65 | } 66 | } else { 67 | this.fpsStart = 0; 68 | this.fpsIndex = 0; 69 | } 70 | }); 71 | } 72 | 73 | initinfoChecker() { 74 | this.infoChecker = setInterval(() => { 75 | if (this.enableinfoChecker) { 76 | this.player.infoPanel.update(); 77 | } 78 | }, 1000); 79 | } 80 | 81 | enable(type) { 82 | this[`enable${type}Checker`] = true; 83 | 84 | if (type === 'fps') { 85 | this.initfpsChecker(); 86 | } 87 | } 88 | 89 | disable(type) { 90 | this[`enable${type}Checker`] = false; 91 | } 92 | 93 | destroy() { 94 | this.types.map((item) => { 95 | this[`enable${item}Checker`] = false; 96 | this[`${item}Checker`] && clearInterval(this[`${item}Checker`]); 97 | return item; 98 | }); 99 | } 100 | } 101 | 102 | export default Timer; 103 | -------------------------------------------------------------------------------- /src/js/user.js: -------------------------------------------------------------------------------- 1 | import utils from './utils'; 2 | 3 | class User { 4 | constructor(player) { 5 | this.storageName = { 6 | opacity: 'dplayer-danmaku-opacity', 7 | volume: 'dplayer-volume', 8 | unlimited: 'dplayer-danmaku-unlimited', 9 | danmaku: 'dplayer-danmaku-show', 10 | subtitle: 'dplayer-subtitle-show', 11 | }; 12 | this.default = { 13 | opacity: 0.7, 14 | volume: player.options.hasOwnProperty('volume') ? player.options.volume : 0.7, 15 | unlimited: (player.options.danmaku && player.options.danmaku.unlimited ? 1 : 0) || 0, 16 | danmaku: 1, 17 | subtitle: 1, 18 | }; 19 | this.data = {}; 20 | 21 | this.init(); 22 | } 23 | 24 | init() { 25 | for (const item in this.storageName) { 26 | const name = this.storageName[item]; 27 | this.data[item] = parseFloat(utils.storage.get(name) || this.default[item]); 28 | } 29 | } 30 | 31 | get(key) { 32 | return this.data[key]; 33 | } 34 | 35 | set(key, value) { 36 | this.data[key] = value; 37 | utils.storage.set(this.storageName[key], value); 38 | } 39 | } 40 | 41 | export default User; 42 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | 2 | const isMobile = /mobile/i.test(window.navigator.userAgent); 3 | const isP2pSupported = isMSESupported() && isWebRTCSupported(); 4 | 5 | const utils = { 6 | /** 7 | * Parse second to time string 8 | * 9 | * @param {Number} second 10 | * @return {String} 00:00 or 00:00:00 11 | */ 12 | secondToTime: (second) => { 13 | second = second || 0; 14 | if (second === 0 || second === Infinity || second.toString() === 'NaN') { 15 | return '00:00'; 16 | } 17 | const add0 = (num) => (num < 10 ? '0' + num : '' + num); 18 | const hour = Math.floor(second / 3600); 19 | const min = Math.floor((second - hour * 3600) / 60); 20 | const sec = Math.floor(second - hour * 3600 - min * 60); 21 | return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':'); 22 | }, 23 | 24 | /** 25 | * control play progress 26 | */ 27 | // get element's view position 28 | getElementViewLeft: (element) => { 29 | let actualLeft = element.offsetLeft; 30 | let current = element.offsetParent; 31 | const elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft; 32 | if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement) { 33 | while (current !== null) { 34 | actualLeft += current.offsetLeft; 35 | current = current.offsetParent; 36 | } 37 | } else { 38 | while (current !== null && current !== element) { 39 | actualLeft += current.offsetLeft; 40 | current = current.offsetParent; 41 | } 42 | } 43 | return actualLeft - elementScrollLeft; 44 | }, 45 | 46 | /** 47 | * optimize control play progress 48 | 49 | * optimize get element's view position,for float dialog video player 50 | * getBoundingClientRect 在 IE8 及以下返回的值缺失 width、height 值 51 | * getBoundingClientRect 在 Firefox 11 及以下返回的值会把 transform 的值也包含进去 52 | * getBoundingClientRect 在 Opera 10.5 及以下返回的值缺失 width、height 值 53 | */ 54 | getBoundingClientRectViewLeft(element) { 55 | const scrollTop = window.scrollY || window.pageYOffset || document.body.scrollTop + ((document.documentElement && document.documentElement.scrollTop) || 0); 56 | 57 | if (element.getBoundingClientRect) { 58 | if (typeof this.getBoundingClientRectViewLeft.offset !== 'number') { 59 | let temp = document.createElement('div'); 60 | temp.style.cssText = 'position:absolute;top:0;left:0;'; 61 | document.body.appendChild(temp); 62 | this.getBoundingClientRectViewLeft.offset = -temp.getBoundingClientRect().top - scrollTop; 63 | document.body.removeChild(temp); 64 | temp = null; 65 | } 66 | const rect = element.getBoundingClientRect(); 67 | const offset = this.getBoundingClientRectViewLeft.offset; 68 | 69 | return rect.left + offset; 70 | } else { 71 | // not support getBoundingClientRect 72 | return this.getElementViewLeft(element); 73 | } 74 | }, 75 | 76 | getScrollPosition() { 77 | return { 78 | left: window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0, 79 | top: window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0, 80 | }; 81 | }, 82 | 83 | setScrollPosition({ left = 0, top = 0 }) { 84 | if (this.isFirefox) { 85 | document.documentElement.scrollLeft = left; 86 | document.documentElement.scrollTop = top; 87 | } else { 88 | window.scrollTo(left, top); 89 | } 90 | }, 91 | 92 | isMobile: isMobile, 93 | 94 | isP2pSupported: isP2pSupported, 95 | 96 | isFirefox: /firefox/i.test(window.navigator.userAgent), 97 | 98 | isChrome: /chrome/i.test(window.navigator.userAgent), 99 | 100 | storage: { 101 | set: (key, value) => { 102 | localStorage.setItem(key, value); 103 | }, 104 | 105 | get: (key) => localStorage.getItem(key), 106 | }, 107 | 108 | nameMap: { 109 | dragStart: isMobile ? 'touchstart' : 'mousedown', 110 | dragMove: isMobile ? 'touchmove' : 'mousemove', 111 | dragEnd: isMobile ? 'touchend' : 'mouseup', 112 | }, 113 | 114 | color2Number: (color) => { 115 | if (color[0] === '#') { 116 | color = color.substr(1); 117 | } 118 | if (color.length === 3) { 119 | color = `${color[0]}${color[0]}${color[1]}${color[1]}${color[2]}${color[2]}`; 120 | } 121 | return (parseInt(color, 16) + 0x000000) & 0xffffff; 122 | }, 123 | 124 | number2Color: (number) => '#' + ('00000' + number.toString(16)).slice(-6), 125 | 126 | number2Type: (number) => { 127 | switch (number) { 128 | case 0: 129 | return 'right'; 130 | case 1: 131 | return 'top'; 132 | case 2: 133 | return 'bottom'; 134 | default: 135 | return 'right'; 136 | } 137 | }, 138 | }; 139 | 140 | function getMediaSource () { 141 | if (typeof window !== 'undefined') { 142 | return window.MediaSource || window.WebKitMediaSource; 143 | } 144 | } 145 | 146 | function isMSESupported () { 147 | const mediaSource = getMediaSource(); 148 | const sourceBuffer = window.SourceBuffer || window.WebKitSourceBuffer; 149 | const isTypeSupported = mediaSource && 150 | typeof mediaSource.isTypeSupported === 'function' && 151 | mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"'); 152 | 153 | // if SourceBuffer is exposed ensure its API is valid 154 | // safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible 155 | const sourceBufferValidAPI = !sourceBuffer || 156 | (sourceBuffer.prototype && 157 | typeof sourceBuffer.prototype.appendBuffer === 'function' && 158 | typeof sourceBuffer.prototype.remove === 'function'); 159 | return !!isTypeSupported && !!sourceBufferValidAPI; 160 | } 161 | 162 | function isWebRTCSupported() { 163 | const browserRTC = getBrowserRTC(); 164 | return (browserRTC && (browserRTC.RTCPeerConnection.prototype.createDataChannel !== undefined)); 165 | } 166 | 167 | function getBrowserRTC () { 168 | if (typeof window === 'undefined') return null 169 | var wrtc = { 170 | RTCPeerConnection: window.RTCPeerConnection || window.mozRTCPeerConnection || 171 | window.webkitRTCPeerConnection, 172 | RTCSessionDescription: window.RTCSessionDescription || 173 | window.mozRTCSessionDescription || window.webkitRTCSessionDescription, 174 | RTCIceCandidate: window.RTCIceCandidate || window.mozRTCIceCandidate || 175 | window.webkitRTCIceCandidate 176 | } 177 | if (!wrtc.RTCPeerConnection) return null 178 | return wrtc 179 | } 180 | 181 | export default utils; 182 | -------------------------------------------------------------------------------- /src/template/player.art: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ include './video.art' video }} 4 | {{ if options.logo }} 5 | 8 | {{ /if }} 9 |
10 |
11 |
12 |
13 |
14 | 15 | {{ if options.danmaku }} 16 | {{ tran('Danmaku is loading') }} 17 | {{ /if }} 18 | {{@ icons.loading }} 19 |
20 |
21 |
22 |
23 |
24 | 27 |
28 |
29 |
{{ tran('Set danmaku color') }}
30 | 34 | 38 | 42 | 46 | 50 | 54 |
55 |
56 |
{{ tran('Set danmaku type') }}
57 | 61 | 65 | 69 |
70 |
71 | 72 | 75 |
76 |
77 | 80 |
81 | 84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 | 0:00 / 94 | 0:00 95 | 96 | {{ if options.live }} 97 | {{ tran('Live') }} 98 | {{ /if }} 99 |
100 |
101 | {{ if options.video.quality }} 102 |
103 | 104 |
105 |
106 | {{ each options.video.quality }} 107 |
{{ $value.name }}
108 | {{ /each }} 109 |
110 |
111 |
112 | {{ /if }} 113 | {{ if options.screenshot }} 114 |
115 | {{@ icons.camera }} 116 |
117 | {{ /if }} 118 | {{ if options.airplay }} 119 |
120 | {{@ icons.airplay }} 121 |
122 | {{ /if }} 123 |
124 | 127 |
128 | {{ if options.subtitle }} 129 |
130 | 133 |
134 | {{ /if }} 135 |
136 | 139 |
140 |
141 |
142 | {{ tran('Speed') }} 143 |
{{@ icons.right }}
144 |
145 |
146 | {{ tran('Loop') }} 147 |
148 | 149 | 150 |
151 |
152 |
153 | {{ tran('Show danmaku') }} 154 |
155 | 156 | 157 |
158 |
159 |
160 | {{ tran('Unlimited danmaku') }} 161 |
162 | 163 | 164 |
165 |
166 |
167 | {{ tran('Opacity for danmaku') }} 168 |
169 |
170 |
171 | 172 |
173 |
174 |
175 |
176 |
177 |
178 | {{ each options.playbackSpeed }} 179 |
180 | {{ $value === 1 ? tran('Normal') : $value }} 181 |
182 | {{ /each }} 183 |
184 |
185 |
186 |
187 | 190 | 193 |
194 |
195 |
196 | 197 |
198 |
199 |
200 |
201 | 202 |
203 |
204 |
205 |
206 |
207 |
[x]
208 |
209 | Player version 210 | 211 |
212 |
213 | Decoder 214 | 215 |
216 |
217 | Player FPS 218 | 219 |
220 |
221 | Video type 222 | 223 |
224 |
225 | Video url 226 | 227 |
228 |
229 | Video resolution 230 | 231 |
232 |
233 | Video duration 234 | 235 |
236 | {{ if options.danmaku }} 237 |
238 | Danmaku id 239 | 240 |
241 |
242 | Danmaku api 243 | 244 |
245 |
246 | Danmaku amount 247 | 248 |
249 | {{ /if }} 250 |
251 | P2P Version 252 | 253 |
254 |
255 | P2P Downloaded 256 | 257 |
258 |
259 | P2P Ratio 260 | 261 |
262 |
263 | P2P Uploaded 264 | 265 |
266 |
267 | Peer Id 268 | 269 |
270 |
271 | Peers Connected 272 | 273 |
274 |
275 |
276 | {{ each options.contextmenu }} 277 |
278 | {{ tran($value.text) }} 279 |
280 | {{ /each }} 281 |
282 |
283 | 286 | -------------------------------------------------------------------------------- /src/template/video.art: -------------------------------------------------------------------------------- 1 | {{ set enableSubtitle = subtitle && subtitle.type === 'webvtt' }} 2 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const GitRevisionPlugin = require('git-revision-webpack-plugin'); 4 | const gitRevisionPlugin = new GitRevisionPlugin(); 5 | const autoprefixer = require('autoprefixer'); 6 | const cssnano = require('cssnano'); 7 | 8 | module.exports = { 9 | mode: 'development', 10 | 11 | // devtool: 'cheap-module-source-map', 12 | 13 | entry: { 14 | CBPlayer: './src/js/index.js', 15 | }, 16 | 17 | output: { 18 | path: path.resolve(__dirname, '..', 'dist'), 19 | filename: '[name].js', 20 | library: '[name]', 21 | libraryTarget: 'umd', 22 | libraryExport: 'default', 23 | umdNamedDefine: true, 24 | publicPath: '/', 25 | }, 26 | 27 | resolve: { 28 | modules: ['node_modules'], 29 | extensions: ['.js', '.scss'], 30 | }, 31 | 32 | module: { 33 | strictExportPresence: true, 34 | rules: [ 35 | { 36 | test: /\.js$/, 37 | use: [ 38 | { 39 | loader: 'babel-loader', 40 | options: { 41 | cacheDirectory: true, 42 | presets: ['@babel/preset-env'], 43 | }, 44 | }, 45 | ], 46 | }, 47 | { 48 | test: /\.scss$/, 49 | use: [ 50 | 'style-loader', 51 | { 52 | loader: 'css-loader', 53 | options: { 54 | importLoaders: 1, 55 | }, 56 | }, 57 | { 58 | loader: 'postcss-loader', 59 | options: { 60 | plugins: [autoprefixer, cssnano], 61 | }, 62 | }, 63 | 'sass-loader', 64 | ], 65 | }, 66 | { 67 | test: /\.(png|jpg)$/, 68 | loader: 'url-loader', 69 | options: { 70 | limit: 40000, 71 | }, 72 | }, 73 | { 74 | test: /\.svg$/, 75 | loader: 'svg-inline-loader', 76 | }, 77 | { 78 | test: /\.art$/, 79 | loader: 'art-template-loader', 80 | }, 81 | ], 82 | }, 83 | 84 | devServer: { 85 | compress: true, 86 | contentBase: path.resolve(__dirname, '..', 'demo'), 87 | clientLogLevel: 'none', 88 | quiet: false, 89 | open: true, 90 | historyApiFallback: { 91 | disableDotRule: true, 92 | }, 93 | watchOptions: { 94 | ignored: /node_modules/, 95 | }, 96 | }, 97 | 98 | plugins: [ 99 | new webpack.DefinePlugin({ 100 | DPLAYER_VERSION: `"${require('../package.json').version}"`, 101 | GIT_HASH: JSON.stringify(gitRevisionPlugin.version()), 102 | }), 103 | ], 104 | 105 | node: { 106 | dgram: 'empty', 107 | fs: 'empty', 108 | net: 'empty', 109 | tls: 'empty', 110 | }, 111 | 112 | performance: { 113 | hints: false, 114 | }, 115 | }; 116 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const GitRevisionPlugin = require('git-revision-webpack-plugin'); 4 | const gitRevisionPlugin = new GitRevisionPlugin(); 5 | const autoprefixer = require('autoprefixer'); 6 | const cssnano = require('cssnano'); 7 | 8 | module.exports = { 9 | mode: 'production', 10 | 11 | bail: true, 12 | 13 | // devtool: 'source-map', 14 | 15 | entry: { 16 | CBPlayer: './src/js/index.js', 17 | }, 18 | 19 | output: { 20 | path: path.resolve(__dirname, '..', 'dist'), 21 | filename: '[name].min.js', 22 | library: '[name]', 23 | libraryTarget: 'umd', 24 | libraryExport: 'default', 25 | umdNamedDefine: true, 26 | publicPath: '/', 27 | }, 28 | 29 | resolve: { 30 | modules: ['node_modules'], 31 | extensions: ['.js', '.scss'], 32 | }, 33 | 34 | module: { 35 | strictExportPresence: true, 36 | rules: [ 37 | { 38 | test: /\.js$/, 39 | use: [ 40 | 'template-string-optimize-loader', 41 | { 42 | loader: 'babel-loader', 43 | options: { 44 | cacheDirectory: true, 45 | presets: ['@babel/preset-env'], 46 | }, 47 | }, 48 | ], 49 | }, 50 | { 51 | test: /\.scss$/, 52 | use: [ 53 | 'style-loader', 54 | { 55 | loader: 'css-loader', 56 | options: { 57 | importLoaders: 1, 58 | }, 59 | }, 60 | { 61 | loader: 'postcss-loader', 62 | options: { 63 | plugins: [autoprefixer, cssnano], 64 | }, 65 | }, 66 | 'sass-loader', 67 | ], 68 | }, 69 | { 70 | test: /\.(png|jpg)$/, 71 | loader: 'url-loader', 72 | options: { 73 | limit: 40000, 74 | }, 75 | }, 76 | { 77 | test: /\.svg$/, 78 | loader: 'svg-inline-loader', 79 | }, 80 | { 81 | test: /\.art$/, 82 | loader: 'art-template-loader', 83 | }, 84 | ], 85 | }, 86 | 87 | plugins: [ 88 | new webpack.DefinePlugin({ 89 | DPLAYER_VERSION: `"${require('../package.json').version}"`, 90 | GIT_HASH: JSON.stringify(gitRevisionPlugin.version()), 91 | }), 92 | ], 93 | 94 | node: { 95 | dgram: 'empty', 96 | fs: 'empty', 97 | net: 'empty', 98 | tls: 'empty', 99 | }, 100 | }; 101 | --------------------------------------------------------------------------------