├── .gitbook.yaml ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── demo ├── assets │ ├── diagrams │ │ └── errorhandling_overview.drawio │ └── images │ │ ├── 00_OvenPlayer_Favicon.svg │ │ ├── 05_OvenSpace_230214.png │ │ ├── OP_LLHLS_220610.svg │ │ └── OvenPlayer_Overview_GitHub_220117.svg ├── demo.html ├── demo_input.html └── ome_demo.html ├── dist ├── RTCTransformWorker.worker.worker.js ├── RTCTransformWorker.worker.worker.js.map ├── ovenplayer.js └── ovenplayer.js.map ├── docs ├── .gitbook │ └── assets │ │ ├── components.png │ │ ├── custom_ui.png │ │ ├── demo_player.png │ │ ├── errorhandling_protocol.drawio.png │ │ ├── ovenplayer.png │ │ ├── ovenplayer_0.9.png │ │ ├── player_template.png │ │ └── web_inspector.png ├── README.md ├── SUMMARY.md ├── api-reference │ ├── api.md │ └── events.md ├── builds.md ├── customize.md ├── error-handling.md ├── examples │ ├── ads.md │ ├── captions.md │ ├── playlist.md │ └── runs-on-webserver.md └── initialization.md ├── package-lock.json ├── package.json ├── packages ├── react │ ├── .gitignore │ ├── README.md │ ├── dist │ │ ├── ovenplayer-react.es.js │ │ ├── ovenplayer-react.umd.js │ │ └── types │ │ │ ├── OvenPlayer.d.ts │ │ │ └── index.d.ts │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── src │ │ ├── OvenPlayer.tsx │ │ ├── index.ts │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock └── vue3 │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── dist │ ├── ovenplayer-vue3.es.js │ ├── ovenplayer-vue3.umd.js │ └── types │ │ ├── OvenPlayer.vue.d.ts │ │ └── index.d.ts │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── OvenPlayer.vue │ ├── index.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock ├── src ├── assets │ ├── fonts │ │ ├── fontello.eot │ │ ├── fontello.svg │ │ ├── fontello.ttf │ │ ├── fontello.woff │ │ ├── fontello.woff2 │ │ ├── seek-icons.eot │ │ ├── seek-icons.svg │ │ ├── seek-icons.ttf │ │ ├── seek-icons.woff │ │ └── seek-icons.woff2 │ └── images │ │ ├── ic-player-close.svg │ │ ├── ic-player-frame-jump.eot │ │ ├── ic-player-frame-jump.svg │ │ ├── ic-player-frame-jump.ttf │ │ ├── ic-player-frame-jump.woff │ │ ├── ic-player-fullscreen-compress.svg │ │ ├── ic-player-fullscreen-expand.svg │ │ ├── ic-player-left.svg │ │ ├── ic-player-non-thumb.svg │ │ ├── ic-player-play-large.svg │ │ ├── ic-player-play.svg │ │ ├── ic-player-playlist.svg │ │ ├── ic-player-re-large.svg │ │ ├── ic-player-right.svg │ │ ├── ic-player-setting.svg │ │ ├── ic-player-stop-large.svg │ │ ├── ic-player-stop.svg │ │ ├── ic-player-volume-2.svg │ │ ├── ic-player-volume-mute.svg │ │ ├── ic-player-volume.svg │ │ └── small │ │ ├── ic-player-fullscreen-compress.svg │ │ ├── ic-player-fullscreen-expand.svg │ │ ├── ic-player-play.svg │ │ ├── ic-player-playlist.svg │ │ ├── ic-player-setting.svg │ │ ├── ic-player-stop.svg │ │ ├── ic-player-volume-2.svg │ │ ├── ic-player-volume-mute.svg │ │ └── ic-player-volume.svg ├── js │ ├── api │ │ ├── Api.js │ │ ├── ApiExpansions.js │ │ ├── Configurator.js │ │ ├── EventEmitter.js │ │ ├── SupportChecker.js │ │ ├── ads │ │ │ ├── ima │ │ │ │ ├── Ad.js │ │ │ │ └── Listener.js │ │ │ ├── utils.js │ │ │ └── vast │ │ │ │ ├── Ad.js │ │ │ │ └── Listener.js │ │ ├── caption │ │ │ ├── Loader.js │ │ │ ├── Manager.js │ │ │ └── parser │ │ │ │ ├── SmiParser.js │ │ │ │ ├── SrtParser.js │ │ │ │ └── VttParser.js │ │ ├── constants.js │ │ ├── media │ │ │ └── Manager.js │ │ ├── playlist │ │ │ └── Manager.js │ │ ├── provider │ │ │ ├── Controller.js │ │ │ ├── html5 │ │ │ │ ├── Listener.js │ │ │ │ ├── Provider.js │ │ │ │ └── providers │ │ │ │ │ ├── Dash.js │ │ │ │ │ ├── Hls.js │ │ │ │ │ ├── Html5.js │ │ │ │ │ ├── WebRTC.js │ │ │ │ │ └── WebRTCLoader.js │ │ │ └── utils.js │ │ └── worker │ │ │ └── RTCTransformWorker.worker.js │ ├── ovenplayer.js │ ├── ovenplayer.sdk.js │ ├── utils │ │ ├── adapter.js │ │ ├── browser.js │ │ ├── captions │ │ │ ├── vttCue.js │ │ │ └── vttRegion.js │ │ ├── date.js │ │ ├── deepMerge.js │ │ ├── eme.js │ │ ├── getTouchSection.js │ │ ├── likeA$.js │ │ ├── logger.js │ │ ├── polyfills │ │ │ └── dom.js │ │ ├── resize-sensor.js │ │ ├── sizeHumanizer.js │ │ ├── strings.js │ │ ├── underscore.js │ │ ├── validator.js │ │ ├── vast-client.js │ │ └── webpack.js │ ├── version.js │ └── view │ │ ├── components │ │ ├── controls │ │ │ ├── frameButtons.js │ │ │ ├── frameButtonsTemplate.js │ │ │ ├── fullScreenButton.js │ │ │ ├── fullScreenButtonTemplate.js │ │ │ ├── main.js │ │ │ ├── mainTemplate.js │ │ │ ├── playButton.js │ │ │ ├── playButtonTemplate.js │ │ │ ├── playlistPanel.js │ │ │ ├── playlistPanelTemplate.js │ │ │ ├── progressBar.js │ │ │ ├── progressBarTemplate.js │ │ │ ├── settingButton.js │ │ │ ├── settingButtonTemplate.js │ │ │ ├── settingPanel │ │ │ │ ├── audioTrackPanel.js │ │ │ │ ├── captionPanel.js │ │ │ │ ├── main.js │ │ │ │ ├── mainTemplate.js │ │ │ │ ├── qualityPanel.js │ │ │ │ ├── sourcePanel.js │ │ │ │ ├── speedPanel.js │ │ │ │ ├── timeDisplayPanel.js │ │ │ │ └── zoomPanel.js │ │ │ ├── timeDisplay.js │ │ │ ├── timeDisplayTemplate.js │ │ │ ├── volumeButton.js │ │ │ └── volumeButtonTemplate.js │ │ └── helpers │ │ │ ├── bigButton.js │ │ │ ├── bigButtonTemplate.js │ │ │ ├── captionViewer.js │ │ │ ├── captionViewerTemplate.js │ │ │ ├── contextPanel.js │ │ │ ├── contextPanelTemplate.js │ │ │ ├── main.js │ │ │ ├── mainTemplate.js │ │ │ ├── messageBox.js │ │ │ ├── messageBoxTemplate.js │ │ │ ├── spinner.js │ │ │ ├── spinnerTemplate.js │ │ │ ├── thumbnail.js │ │ │ ├── thumbnailTemplate.js │ │ │ ├── waterMark.js │ │ │ └── waterMarkTemplate.js │ │ ├── engine │ │ ├── OvenTemplate.js │ │ └── Templates.js │ │ ├── example │ │ ├── textview.js │ │ └── textviewTemplate.js │ │ ├── global │ │ └── PanelManager.js │ │ ├── view.js │ │ └── viewTemplate.js └── stylesheet │ └── ovenplayer.less ├── webpack.config.js └── webpack.development.js /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: airensoft 2 | open_collective: ovenmediaengine 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/** 3 | !src/** -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | ovenplayer.com -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by opening an issue or contacting with project maintainers. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | First of all, thank you for your interest in OvenMediaEngine and OvenPlayer! 3 | We need your help to keep and develop our open-source projects, and we want to tell you that you can contribute in many ways. 4 | 5 | Thank you! 6 | 7 | ## How to contribute 8 | - [Finding Bugs](#finding-bugs) 9 | - [Reviewing Code](#reviewing-code) 10 | - [Sharing Ideas](#sharing-ideas) 11 | - [Testing](#testing) 12 | - [Improving Documentation](#improving-documentation) 13 | - [Spreading & Use Cases](#spreading--use-cases) 14 | - [Recurring Donations](#recurring-donations) 15 | 16 | And all other contributions that make our project better! 17 | 18 | ## Communication 19 | The first communication channel we use is GitHub Issues. If you have any problem or question using OvenMediaEngine and OvenPlayer, please describe it on AirenSoft GitHub Issues. Our open-source community is lively, so developers who see your inquiry will respond as soon as possible. 20 | Also, if your question helped someone who uses our open-source community, we think it's a great contribution. 21 | 22 | --- 23 | ### Finding Bugs 24 | If you find any bugs while using OvenMediaEngine and OvenPlayer, please feel free to let us know through GitHub Issues. Also, we may ask you for help in reproducing it and testing the fixed code. We will do our best to reproduce and fix the bug. 25 | 26 | ### Reviewing Code 27 | We review our code countless times before release, but we can make mistakes. So let us know if you find it. Also, if you know any code or structure that OvenMediaEngine and OvenPlayer can work on more efficiently, please let us know. 28 | 29 | ### Sharing Ideas 30 | If you have any ideas about technology trends that can advance OvenMediaEngine and OvenPlayer and features that will work synergistically, please request us. We are ready to review and embrace it for development. 31 | 32 | ### Testing 33 | After the OvenMediaEngine and OvenPlayer updates, we may request you to test as we have responsibility for ensuring stability in more environments. Therefore, we would be very grateful if you could help us run the test. 34 | - [For OvenMediaEngine](https://github.com/AirenSoft/OvenMediaEngine/issues) 35 | - [For OvenPlayer](https://github.com/AirenSoft/OvenPlayer/issues) 36 | 37 | --- 38 | ### Improving Documentation 39 | We are not a perfectly English-speaking team. Therefore, there may be typos, grammatical awkwardness, or incomprehensible sentences in our documents. Please let us know if you find them. 40 | Or, if you would like to translate our documents into a language other than English, we welcome your work! 41 | - [OvenMediaEngine GitBook](https://airensoft.gitbook.io/ovenmediaengine/) 42 | - [OvenPlayer GitBook](https://airensoft.gitbook.io/ovenplayer/) 43 | 44 | --- 45 | ### Spreading & Use Cases 46 | If you like our open-source project, if you have made something with our open-source, or if our open-source has helped you, please let people know about OvenMediaEngine and OvenPlayer. 47 | And we are love to hear about your experience and story using OvenMediaEngine and OvenPlayer, like why you chose this, how to use this, and more. You know, the voices of real contributors are of great help to our project. 48 | - 49 | 50 | --- 51 | ### Recurring Donations 52 | If you want to help us continue developing OvenMediaEngine and OvenPlayer, or if our open-source has helped your business, please support us through GitHub Sponsor or Open Collective. 53 | Your financial contribution to OvenMediaEngine and OvenPlayer will be used to reward the developer's efforts, cover the cost of servers and hardware that are continuously used, and attend related exhibitions. And these are transparent, publicly visible, and is used to develop our open-source projects. 54 | - [GitHub Sponsor](https://github.com/AirenSoft) 55 | - [Open Collective](https://opencollective.com/ovenmediaengine) 56 | 57 | --- 58 | We always hope that OvenMediaEngine will make your project a success! 59 | 60 | Thank you! 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 AirenSoft Co., Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OvenPlayer 2 | 3 | ## What is OvenPlayer? 4 | 5 | 6 | OvenPlayer is a JavaScript-based Player that can play Low Latency HLS (LLHLS) and WebRTC streams optimized for [OvenMediaEngine](https://github.com/AirenSoft/OvenMediaEngine). It provides various APIs, so you can build and operate your media service more easily. 7 | 8 | ## Demo 9 | 10 | 11 | OvenSpace is a sub-second latency streaming demo service using [OvenMediaEngine](https://github.com/AirenSoft/OvenMediaEngine), [OvenPlayer](https://github.com/AirenSoft/OvenPlayer) and [OvenLiveKit](https://github.com/AirenSoft/OvenLiveKit-Web). You can experience OvenPlayer in the **[OvenSpace Demo](https://space.ovenplayer.com/)** and see examples of applying in [OvenSpace Repository](https://github.com/AirenSoft/OvenSpace). 12 | 13 | ## Features 14 | * HTML5 Standard Video Player 15 | * Live Streaming with Various Protocols 16 | * Sub-Second Latency: WebRTC (Signalling Protocol Conforms to the OME Specification) 17 | * Low Latency: LLHLS, LLDASH (Chunked CMAF) 18 | * Legacy: HLS, MPEG-DASH 19 | * Automatic Fallback Streaming 20 | * Fully Customizable UI and SDK 21 | * Insert ADs with Various Formats< 22 | * VAST4, VAST3, VAST2, VPAID2 (HTML5), VMAP1.0.1 23 | * Support Multiple Subtitle Formats 24 | * SMI, VTT, SRT 25 | 26 | ## Quick Start 27 | Please read the [Quick Start](https://airensoft.gitbook.io/ovenplayer/#quick-start). 28 | 29 | ### OvenPlayer Demo 30 | * Without TLS: http://demo.ovenplayer.com 31 | * With TLS: https://demo.ovenplayer.com 32 | 33 | ## API and Configuration 34 | Start the [Demo](https://demo.ovenplayer.com) and see [API and Configuration](https://airensoft.gitbook.io/ovenplayer/initialization) for more details. 35 | 36 | ## Build the source code 37 | Check out [Build](https://airensoft.gitbook.io/ovenplayer/builds) section how to create `ovenplayer.js` for deployment or development after modifying the source code. 38 | 39 | ## How to contribute 40 | We appreciate your interest in OvenPlayer. 41 | 42 | We need your help to keep and develop our open-source project, and we want to tell you that you can contribute in many ways. Please read our [Guidelines](CONTRIBUTING.md), [Rules](CODE_OF_CONDUCT.md), and [Contribute](https://www.ovenmediaengine.com/contribute). 43 | 44 | - [Finding Bugs](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#finding-bugs) 45 | - [Reviewing Code](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#reviewing-code) 46 | - [Sharing Ideas](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#sharing-ideas) 47 | - [Testing](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#testing) 48 | - [Improving Documentation](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#improving-documentation) 49 | - [Spreading & Use Cases](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#spreading--use-cases) 50 | - [Recurring Donations](https://github.com/AirenSoft/OvenPlayer/blob/master/CONTRIBUTING.md#recurring-donations) 51 | 52 | We always hope that OvenPlayer will make your project a success. 53 | 54 | ## For more information 55 | * [AirenSoft Website](https://airensoft.com) 56 | * About OvenMediaEngine, OvenMediaEngine Enterprise, OvenVideo, AirenBlog and more 57 | * [OvenPlayer Getting Started](https://airensoft.gitbook.io/ovenplayer) 58 | * User guide for OvenPlayer UI Customize, API Reference, Examples, and more 59 | * [OvenMediaEngine GitHub](https://github.com/AirenSoft/OvenMediaEngine) 60 | * Sub-Second Latency Streaming Server with LLHLS and WebRTC 61 | * [OvenMediaEngine Getting Started](https://airensoft.gitbook.io/ovenmediaengine/) 62 | * User guide for OvenMediaEngine Configuration, ABR, Clustering, and more 63 | * [OvenLiveKit](https://github.com/AirenSoft/OvenLiveKit-Web) 64 | * JavaScript-based Live Streaming Encoder for OvenMediaEngine 65 | * [OvenSpace Demo](https://space.ovenplayer.com/) 66 | * Sub-Second Latency Streaming Demo Service 67 | 68 | ## License 69 | OvenPlayer is licensed under the [MIT license](LICENSE). 70 | 71 | ## About AirenSoft 72 | AirenSoft aims to make it easier for you to build a stable broadcasting/streaming service with Sub-Second Latency. 73 | Therefore, we will continue developing and providing the most optimized tools for smooth Sub-Second Latency Streaming. 74 | 75 | Would you please click on each link below for details: 76 | * ["JavaScript-based Live Streaming Encoder" **OvenLiveKit**](https://github.com/AirenSoft/OvenLiveKit-Web) 77 | * ["Sub-Second Latency Streaming Server with LLHLS and WebRTC" **OvenMediaEngine**](https://github.com/AirenSoft/OvenMediaEngine) 78 | * ["JavaScript-based Player with LLHLS and WebRTC" **OvenPlayer**](https://github.com/AirenSoft/OvenPlayer) 79 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf dist/production/ovenplayer 4 | npm run build 5 | cp src/assets/OvenPlayerFlash.swf dist/production/ovenplayer 6 | git add dist/production/ovenplayer 7 | -------------------------------------------------------------------------------- /demo/assets/diagrams/errorhandling_overview.drawio: -------------------------------------------------------------------------------- 1 | 3Vtdb5s8FP41XC7iO+nl2qbbpG2a1lfa26vJBQe8EYyMacJ+/Wwwny4NzZIYKk1afDgYc87zHD82rmbdbPcfCEjCL9iHkWbq/l6zbjXTNGzT1Pg/3c9Ly9IVhoAgXzg1hnv0BwqjLqwZ8mHacaQYRxQlXaOH4xh6tGMDhOBd122Do+5TExCIJ+qN4d4DEZTcfiCfhqV1ZS4b+0eIgrB6suFelVe2oHIWHach8PGuZbLWmnVDMKblr+3+BkY8eFVcyvvuBq7WAyMwpmNu8J315oePUvT9D8ySgH6htw/vxGBTmlcvDH32/qKJCQ1xgGMQrRvrNcFZ7EPeq85ajc9njBNmNJjxF6Q0F8kEGcXMFNJtJK7CPaL/89sXjmg9tK7c7kXPRSNvNb5BgraQQlLZYkryVke8+dC+1nRVtKq+ynfmLzoYSmFKcUY8+EL8KkgCEkD6gp9bJ5wxBWL2FiRn9xEYAYqeuuMAArJB7ddklf0QiX1FkkW/TyDKxJM+Y+AXaQeESghIKcG/a7Sb3dwxFCfcb7sPOOEXmwjvvJB1syg6+8l9diGi8D4BReR2zK2O+RMkFO5fjrocJXHDShAp7zZ3DS2Nimthi5K2fq6w6lLopkqeI4lStPqsOyF73JHsMQaAMZo+4tZvGLEh1oAy7C6iTKsHlXJg4q4eWuphHA8gd4iXG0RS2gSwj7IGQ0Nsa+EFPKY4yih8TzyBqcLatGzOaonyp+er4Son7FIK+H3meTBNZ0Pkic6CY6dBBjaF86DtSPm/AyhSmXyjlfoGCMckv5P6hekcyP4Gx7SqAKYiNAzVjwupomFZJIqCdXcqbeRDjz0dxxcTR3URPVRsV2crtvNZWkxUHZXFckxVXSnlkS3PquW6Qk8ikD8C7/cbUTC2rlrBVLsprVivCcHkLU5hy0NT2CmptprFlGVdSen/inkMVGNAlYZ9tgSfU9qMxYm1VFqSV0NlQme1lqXedCM2+utHZnEDWsTIBVted+PHlP9XV+4ZayB77A7R2TSQbUjBm2phHijCh2rwpQlojl1b2PrzULkMAU15bfEfyVEc8KHjYhgeZlhvareuOdeG5tzOVSpNTytZ8m7PDQMmirMyD4PidCb7367yCDsyylXvp3SFiP5yzXuFEDEmVgernB6sg47SOlgNs4WQ73Xlm+M2i937ZGAp1xiOJYWYrwlmIjtesR6cmhRxRlPQVElBR54GH+CEPni83Ro9HiBKTwc4co3+lHJBKi0Si5Sa+gZEUSmcnvFJ+ZvOuKCbY79Snm/RKH8WPv4r5RT2dAztVXW8z8hu3VGhof75q2Vx63tCQN5ySPixgrTVc+90gqN3ken0z3f1/GvkHulvd/3Zj3LEJz3y4Mgb2OWSoVwWz25N5vaSpP5UkiPv/b0VPbgw7O7ucNswlUl/7Jkmtaqw4r4iBXgqSPzj1KIk71dKxZ4sLj4C9lBTj+G+deRszhLOUH72oTrjPoX11QXPPpyTXldjtZrSD7TVMOUzRV169dEx003+cx7pZM3mbyNK+dn8hYm1/gs= -------------------------------------------------------------------------------- /demo/assets/images/00_OvenPlayer_Favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/assets/images/05_OvenSpace_230214.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/demo/assets/images/05_OvenSpace_230214.png -------------------------------------------------------------------------------- /dist/RTCTransformWorker.worker.worker.js: -------------------------------------------------------------------------------- 1 | !function(){function t(t,r=""){return Array.from(t,(t=>("0"+(255&t).toString(16)).slice(-2))).join(r)}function r(r){const e=t(r);return[e.slice(0,8),e.slice(8,12),e.slice(12,16),e.slice(16,20),e.slice(20,24),e.slice(24,32)].join("-")}function e(r){const e=t(r);return parseInt(e,16)}function n(t,r){for(;r=r))break;{const s=n(t,a+(1===t[a+2]?3:4)+1);if(!(s>a)){e.push(t.subarray(a));break}e.push(t.subarray(a,s)),r=s}}return e})(new Uint8Array(a.data)).forEach((n=>{const a=1===n[2]?3:4;if(6==(31&n[a])){const s=function(t){const r=[];let e=0;const n=128===t[t.length-1]?t.length-1:t.length;for(;e2&&0===t[e-2]&&0===t[e-1]&&3===t[e]||r.push(t[e]);return new Uint8Array(r)}(n.subarray(a+1))),o={nalu:n,sei:s};"464d4c475241494e434f4c4f55524201"===t(s.payload.subarray(0,16))?postMessage({action:"sei",data:{...o,registered:!0,uuid:r(s.payload.subarray(0,16)),timecode:e(s.payload.subarray(16,24)),userdata:s.payload.subarray(24)}}):postMessage({action:"sei",data:{...o,registered:!1}})}})),s.enqueue(a)}})}function s({readable:t,writable:r},e){t.pipeThrough(e).pipeTo(r)}addEventListener("rtctransform",(t=>{s(t.transformer,a())})),addEventListener("message",(t=>{const{action:r}=t.data;"rtctransform"===r&&s(t.data,a())}))}(); 2 | //# sourceMappingURL=RTCTransformWorker.worker.worker.js.map -------------------------------------------------------------------------------- /docs/.gitbook/assets/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/components.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/custom_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/custom_ui.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/demo_player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/demo_player.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/errorhandling_protocol.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/errorhandling_protocol.drawio.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/ovenplayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/ovenplayer.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/ovenplayer_0.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/ovenplayer_0.9.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/player_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/player_template.png -------------------------------------------------------------------------------- /docs/.gitbook/assets/web_inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/docs/.gitbook/assets/web_inspector.png -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | * [Initialization](initialization.md) 5 | * [Error Handling](error-handling.md) 6 | * [Builds](builds.md) 7 | * [UI Customize](customize.md) 8 | 9 | ## API Reference 10 | 11 | * [API](api-reference/api.md) 12 | * [Event](api-reference/events.md) 13 | 14 | ## Examples 15 | 16 | * [Playlist](examples/playlist.md) 17 | * [Captions](examples/captions.md) 18 | * [Ads](examples/ads.md) 19 | * [Run-on WebServer](examples/runs-on-webserver.md) 20 | -------------------------------------------------------------------------------- /docs/builds.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This section describes the development and builds process. 3 | --- 4 | 5 | # Builds 6 | 7 | ## How to write code 8 | 9 | OvenPlayer uses [npm](https://www.npmjs.com) and [webpack](https://webpack.js.org) when building. If you are using npm for the first time, please refer to [Install Node.js, npm](https://www.npmjs.com/get-npm). In addition, you need to configure the environment to keep this up-to-date and working. 10 | 11 | ### Environment 12 | 13 | If npm works well on your system, run the following command in Terminal to install the packages needed to develop OvenPlayer, such as webpack. 14 | 15 | ``` 16 | $ npm ci 17 | ``` 18 | 19 | ### Production Build 20 | 21 | If you want to modify the source code, you need to write it manually. 22 | 23 | > If you are cloning a project for the first time, you can find already built files in the `dist/` directories. 24 | 25 | However, you can build your modified source code with the following command. The built source code can be found in the `dist/` directory. 26 | 27 | ``` 28 | npm run build 29 | ``` 30 | 31 | This command allows you to build the webpack automatically whenever the source code is modified. 32 | 33 | ### Development Build 34 | 35 | It is inefficient to build code every time during development and testing. If you use the watch function, webpack detects changes in the source code and automatically builds it quickly. The development built source code can be found in the `dev/` directory. 36 | 37 | ``` 38 | npm run watch 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/error-handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | ## Error handling diagram 4 | 5 | This diagram explains how OvenPlayer handles errors and automatic fallback operations. 6 | 7 | ![](.gitbook/assets/ovenplayer.png) 8 | 9 | ### \[1] Trying to recover error 10 | 11 | Error recovering is different for each protocol. 12 | 13 | #### HLS 14 | 15 | Errors thrown by `hls.js` are differentiated by whether they are fatal or not. For a none fatal error, `hls.js` automatically repairs the error. How to handle it is described [here](https://github.com/video-dev/hls.js/blob/master/docs/design.md#error-detection-and-handling). 16 | 17 | ![](.gitbook/assets/errorhandling\_protocol.drawio.png) 18 | -------------------------------------------------------------------------------- /docs/examples/ads.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This example shows you how to set Ads in OvenPlayer. 3 | --- 4 | 5 | # Ads 6 | 7 | OvenPlayer supports VAST 4, VAST 3, VAST 2, VPAID 2 (HTML 5), and VMAP 1.0.1, so you can easily use various ads. If you need more information, please see [Support and Compatibility](https://developers.google.com/interactive-media-ads/docs/sdks/html5/compatibility). 8 | 9 | {% code title="" %} 10 | ``` 11 | let player = OvenPlayer.create("player", { 12 | adTagUrl : "https://pubads.g.doubleclick.net/gampad/ads?...", 13 | sources : [ 14 | type : "mp4", 15 | file : "https://path.to/your_video", 16 | label : "360P" 17 | ] 18 | }); 19 | ``` 20 | {% endcode %} 21 | -------------------------------------------------------------------------------- /docs/examples/captions.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This example shows how to register and use subtitles without conversion. 3 | --- 4 | 5 | # Captions 6 | 7 | OvenPlayer supports webVTT, SRT, and SMI subtitles, so you can register and use subtitles without conversion. If you want to know more details, please refer to the [Captions API](../api-reference/api.md#getcaptionlist) chapter. 8 | 9 | {% code title="" %} 10 | ``` 11 | let player = OvenPlayer.create("player", {sources : { 12 | type : "mp4", 13 | file : "https://path.to/your_video", 14 | label : "360P" 15 | ], 16 | tracks : [ 17 | { 18 | kind : "captions", 19 | file : "https://path.to/your_caption.vtt", 20 | label : "KO vtt" 21 | }, 22 | { 23 | kind : "captions", 24 | file : "https://path.to/your_caption.srt", 25 | label : "KO srt" 26 | }, 27 | { 28 | kind : "captions", 29 | file : "https://path.to/your_caption.smi", 30 | label : "KO smi" 31 | } 32 | ] 33 | }); 34 | ``` 35 | {% endcode %} 36 | -------------------------------------------------------------------------------- /docs/examples/playlist.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This example shows how to set a playlist in OvenPlayer. 3 | --- 4 | 5 | # Playlist 6 | 7 | ### Source 8 | 9 | It is the smallest unit for playing video from OvenPlayer. It is generally a single content. 10 | 11 | {% code title="example" %} 12 | ``` 13 | let player = OvenPlayer.create("player", { 14 | type : "mp4", 15 | file : "https://path.to/your_video", 16 | framerate : 30, 17 | label : "360P" 18 | }); 19 | ``` 20 | {% endcode %} 21 | 22 | ### Sources 23 | 24 | If you have multiple protocols or multiple resolutions for a single content, you can register them at once using `sources`. 25 | 26 | OvenPlayer will play a video in the order of the protocol or resolution you entered in `sources`, and will automatically play the next source if playback fails. 27 | 28 | {% code title="" %} 29 | ``` 30 | let player = OvenPlayer.create("player", {sources : [ 31 | { 32 | type : "mp4", 33 | file : "https://path.to/your_video", 34 | framerate : 30, 35 | label : "360P" 36 | }, 37 | { 38 | type : "mpd", 39 | file : "https://path.to/your_video.mpd", 40 | framerate : 30, 41 | label: "360P DASH" 42 | }, 43 | { 44 | type : "hls", 45 | file : "https://path.to/your_video.m3u8", 46 | framerate : 30, 47 | label: "360P HLS" 48 | }, 49 | { 50 | type : "rtmp", 51 | file : "rtmp://path.to/your_video", 52 | framerate : 30, 53 | label: "360P RTMP" 54 | } 55 | ] }); 56 | ``` 57 | {% endcode %} 58 | 59 | ### Playlist 60 | 61 | `playlist` has multiple `sources` mentioned above. You can explore between playlists, and it automatically plays the next content. Also, you can assign ads and captions for each `playlist`. 62 | 63 | For more information, please refer to the [Playlist API](../api-reference/api.md#getplaylist) chapter. 64 | 65 | {% code title="" %} 66 | ``` 67 | let player = OvenPlayer.create("player", { 68 | playlist : [ 69 | { 70 | title : "01", 71 | adTagUrl : "https://pubads.g.doubleclick.net/gampad/ads?...", 72 | image : "https://path.to/your_video_thumbnail.jpeg", 73 | duration : 7343, 74 | sources: [ { 75 | type : "mp4", 76 | file : "https://path.to/your_video", 77 | label : "360P" 78 | }], 79 | tracks: [{ 80 | kind : "captions", 81 | file : "https://path.to/your_caption.vtt", 82 | label : "KO vtt" 83 | }] 84 | }, 85 | { 86 | title : "02", 87 | adTagUrl : "https://pubads.g.doubleclick.net/gampad/ads?...", 88 | image : "https://path.to/your_video_thumbnail2.jpeg", 89 | duration : 8333, 90 | sources: [ { 91 | type : "mp4", 92 | file : "https://path.to/your_video2", 93 | label : "360P" 94 | }, 95 | { 96 | type : "mpd", 97 | file : "https://path.to/your_video.mpd", 98 | label: "360P DASH" 99 | } 100 | tracks: [{ 101 | kind : "captions", 102 | file : "https://path.to/your_caption2.vtt", 103 | label : "KO vtt" 104 | }] 105 | } 106 | ] 107 | }); 108 | ``` 109 | {% endcode %} 110 | -------------------------------------------------------------------------------- /docs/examples/runs-on-webserver.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: This example shows you how to build a simple web server using Nginx. 3 | --- 4 | 5 | # Run-on WebServer 6 | 7 | 8 | 9 | {% hint style="success" %} 10 | If you cannot install a web server in your environment, please use our demo player. 11 | 12 | · OvenPlayer for testing without TLS: [http://demo.ovenplayer.com](http://demo.ovenplayer.com) 13 | 14 | · OvenPlayer for testing with TLS: [https://demo.ovenplayer.com](https://demo.ovenplayer.com) 15 | {% endhint %} 16 | 17 | ### Install Nginx 18 | 19 | All descriptions are based on CentOS 7. If you are using a different OS, please see the [Nginx tutorials](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/). 20 | 21 | ``` 22 | sudo yum install epel-release 23 | sudo yum install nginx 24 | ``` 25 | 26 | ### Go to the Nginx root folder 27 | 28 | ``` 29 | cd /usr/share/nginx/html 30 | ``` 31 | 32 | ### Download OvenPlayer 33 | 34 | ``` 35 | git clone https://github.com/AirenSoft/OvenPlayer.git 36 | ``` 37 | 38 | ### Access from your browser 39 | 40 | ``` 41 | http://YOUR_IP/OvenPlayer/docs/demo.html 42 | ``` 43 | 44 | ![](<../.gitbook/assets/demo_player.png>) 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ovenplayer", 3 | "version": "0.10.42", 4 | "description": "OvenPlayer is Open-Source HTML5 Player. OvenPlayer supports WebRTC Signaling from OvenMediaEngine for Sub-Second Latency Streaming.", 5 | "main": "dist/ovenplayer.js", 6 | "scripts": { 7 | "watch": "webpack --config webpack.development.js --watch", 8 | "build": "webpack --config webpack.config.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/AirenSoft/OvenPlayer.git" 13 | }, 14 | "keywords": [ 15 | "Sub-Second Latency Streaming", 16 | "WebRTC", 17 | "OvenMediaEngine", 18 | "Low Latency HTTP", 19 | "MPEG-DASH", 20 | "HLS", 21 | "HTML5", 22 | "Video", 23 | "Player" 24 | ], 25 | "author": "AirenSoft Co., Ltd.", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/AirenSoft/OvenPlayer/issues" 29 | }, 30 | "homepage": "https://www.ovenmediaengine.com/ovenplayer", 31 | "devDependencies": { 32 | "@babel/core": "^7.15.0", 33 | "@babel/preset-env": "^7.15.0", 34 | "babel-loader": "^8.2.2", 35 | "css-loader": "^6.2.0", 36 | "less": "^4.1.1", 37 | "less-loader": "^10.0.1", 38 | "style-loader": "^3.2.1", 39 | "webpack": "^5.51.1", 40 | "webpack-bundle-analyzer": "^3.0.3", 41 | "webpack-cli": "^4.8.0", 42 | "webpack-merge": "^5.8.0", 43 | "worker-loader": "^3.0.8" 44 | }, 45 | "dependencies": { 46 | "core-js": "^3.16.3", 47 | "whatwg-fetch": "^3.6.2" 48 | } 49 | } -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # OvenPlayer-React 2 | 3 | Reusable OvenPlayer component for React.js -------------------------------------------------------------------------------- /packages/react/dist/types/OvenPlayer.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { OvenPlayerConfig, OvenPlayerEvents } from "ovenplayer"; 3 | /** 4 | * Props for the OvenPlayerReact component. 5 | */ 6 | export interface OvenPlayerProps { 7 | /** 8 | * The configuration object for OvenPlayer. 9 | */ 10 | config: OvenPlayerConfig; 11 | /** 12 | * Triggered when the player is fully initialized and ready to use its API methods. 13 | */ 14 | onReady?: () => void; 15 | /** 16 | * Triggered when new metadata is received. 17 | * @param data - `OvenPlayerEvents['metaChanged']` 18 | */ 19 | onMetaChanged?: (data: OvenPlayerEvents["metaChanged"]) => void; 20 | /** 21 | * Triggered when the player's state changes. 22 | * @param data - `OvenPlayerEvents['stateChanged']` 23 | */ 24 | onStateChanged?: (data: OvenPlayerEvents["stateChanged"]) => void; 25 | /** 26 | * Triggered when the player's size changes. 27 | * @param data - `OvenPlayerEvents['resized']` 28 | */ 29 | onResized?: (data: OvenPlayerEvents["resized"]) => void; 30 | /** 31 | * Triggered when the playback rate changes. 32 | * @param data - `OvenPlayerEvents['playbackRateChanged']` 33 | */ 34 | onPlaybackRateChanged?: (data: OvenPlayerEvents["playbackRateChanged"]) => void; 35 | /** 36 | * Triggered after a seek action is requested (e.g., via scrubbing or API). 37 | * @param data - `OvenPlayerEvents['seek']` 38 | */ 39 | onSeek?: (data: OvenPlayerEvents["seek"]) => void; 40 | /** 41 | * Triggered periodically (up to ~10 times per second) while the player is playing, 42 | * providing the current playback position. 43 | * @param data - `OvenPlayerEvents['time']` 44 | */ 45 | onTime?: (data: OvenPlayerEvents["time"]) => void; 46 | /** 47 | * Triggered when the currently playing item loads additional data into its buffer. 48 | * @param data - `OvenPlayerEvents['bufferChanged']` 49 | */ 50 | onBufferChanged?: (data: OvenPlayerEvents["bufferChanged"]) => void; 51 | /** 52 | * Triggered when the player's mute state changes (mute/unmute). 53 | * @param data - `OvenPlayerEvents['mute']` 54 | */ 55 | onMute?: (data: OvenPlayerEvents["mute"]) => void; 56 | /** 57 | * Triggered when the player's volume changes. 58 | * @param data - `OvenPlayerEvents['volumeChanged']` 59 | */ 60 | onVolumeChanged?: (data: OvenPlayerEvents["volumeChanged"]) => void; 61 | /** 62 | * Triggered when the active playlist changes. This could be due to user interaction 63 | * or a script calling `setCurrentPlaylist`, or because the previous playlist completed. 64 | * @param data - `OvenPlayerEvents['playlistChanged']` 65 | */ 66 | onPlaylistChanged?: (data: OvenPlayerEvents["playlistChanged"]) => void; 67 | /** 68 | * Triggered when the active source (protocol) changes. This could be due to user interaction 69 | * (e.g., source menu) or a script calling `setCurrentSource`. 70 | * @param data - `OvenPlayerEvents['sourceChanged']` 71 | */ 72 | onSourceChanged?: (data: OvenPlayerEvents["sourceChanged"]) => void; 73 | /** 74 | * Triggered when the active quality level changes. This could be due to user interaction 75 | * or a script calling `setCurrentQuality`. 76 | * @param data - `OvenPlayerEvents['qualityLevelChanged']` 77 | */ 78 | onQualityLevelChanged?: (data: OvenPlayerEvents["qualityLevelChanged"]) => void; 79 | /** 80 | * Triggered when the VTTCue changes (e.g., subtitles or captions). 81 | * @param data - `OvenPlayerEvents['cueChanged']` 82 | */ 83 | onCueChanged?: (data: OvenPlayerEvents["cueChanged"]) => void; 84 | /** 85 | * Triggered when the time display mode changes (e.g., live vs. VOD). 86 | * @param data - `OvenPlayerEvents['timeDisplayModeChanged']` 87 | */ 88 | onTimeDisplayModeChanged?: (data: OvenPlayerEvents["timeDisplayModeChanged"]) => void; 89 | /** 90 | * Triggered when an advertisement changes. 91 | * @param data - `OvenPlayerEvents['adChanged']` 92 | */ 93 | onAdChanged?: (data: OvenPlayerEvents["adChanged"]) => void; 94 | /** 95 | * Triggered periodically during ad playback, providing ad time information. 96 | * @param data - `OvenPlayerEvents['adTime']` 97 | */ 98 | onAdTime?: (data: OvenPlayerEvents["adTime"]) => void; 99 | /** 100 | * Triggered when an advertisement has completed. 101 | */ 102 | onAdComplete?: () => void; 103 | /** 104 | * Triggered when the screen mode changes (e.g., fullscreen on/off). 105 | * @param data - `OvenPlayerEvents['fullscreenChanged']` 106 | */ 107 | onFullscreenChanged?: (data: OvenPlayerEvents["fullscreenChanged"]) => void; 108 | /** 109 | * Triggered when the player is clicked. 110 | * If the ad is clicked, `{ type: "adclick" }` is returned. 111 | * @param data - `OvenPlayerEvents['clicked']` 112 | */ 113 | onClicked?: (data: OvenPlayerEvents["clicked"]) => void; 114 | /** 115 | * Triggered when the entire playlist finishes (all items ended). 116 | */ 117 | onAllPlaylistEnded?: () => void; 118 | /** 119 | * Triggered when the HLS object has been initialized and is ready for use. 120 | * @param data - `OvenPlayerEvents['hlsPrepared']` 121 | */ 122 | onHlsPrepared?: (data: OvenPlayerEvents["hlsPrepared"]) => void; 123 | /** 124 | * Triggered after the HLS object has been destroyed. 125 | */ 126 | onHlsDestroyed?: () => void; 127 | /** 128 | * Triggered when the DASH object has been initialized and is ready for use. 129 | * @param data - `OvenPlayerEvents['dashPrepared']` 130 | */ 131 | onDashPrepared?: (data: OvenPlayerEvents["dashPrepared"]) => void; 132 | /** 133 | * Triggered after the DASH object has been destroyed. 134 | */ 135 | onDashDestroyed?: () => void; 136 | /** 137 | * Triggered when the player is destroyed. 138 | */ 139 | onDestroy?: () => void; 140 | /** 141 | * Triggered when an error occurs. 142 | * @param data - `OvenPlayerEvents['error']` 143 | */ 144 | onError?: (data: OvenPlayerEvents["error"]) => void; 145 | } 146 | declare const OvenPlayer: React.FC; 147 | export default OvenPlayer; 148 | -------------------------------------------------------------------------------- /packages/react/dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import OvenPlayer from "./OvenPlayer"; 2 | export default OvenPlayer; 3 | export * from "./OvenPlayer"; 4 | -------------------------------------------------------------------------------- /packages/react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OvenPlayer Dev 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ovenplayer-react", 3 | "private": false, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build && tsc -b --emitDeclarationOnly", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "main": "dist/ovenplayer-react.umd.js", 13 | "module": "dist/ovenplayer-react.es.js", 14 | "types": "dist/types/index.d.ts", 15 | "peerDependencies": { 16 | "react": "^18.0.0", 17 | "react-dom": "^18.0.0" 18 | }, 19 | "dependencies": { 20 | "hls.js": "^1.5.20", 21 | "ovenplayer": "^0.10.38" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.19.0", 25 | "@types/node": "^22.13.4", 26 | "@types/ovenplayer": "^0.10.12", 27 | "@types/react": "^18.0.28", 28 | "@types/react-dom": "^18.0.11", 29 | "@vitejs/plugin-react": "^4.3.4", 30 | "eslint": "^9.19.0", 31 | "eslint-plugin-react-hooks": "^5.0.0", 32 | "eslint-plugin-react-refresh": "^0.4.18", 33 | "globals": "^15.14.0", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "typescript": "~5.7.2", 37 | "typescript-eslint": "^8.22.0", 38 | "vite": "^6.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | import OvenPlayer from "./OvenPlayer"; 2 | export default OvenPlayer; 3 | 4 | export * from "./OvenPlayer"; 5 | -------------------------------------------------------------------------------- /packages/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | 15 | "noEmit": false, 16 | 17 | "declaration": true, 18 | "declarationDir": "./dist/types", 19 | 20 | "jsx": "react-jsx", 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noUncheckedSideEffectImports": true 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "target": "ES2022", 6 | "lib": [ 7 | "ES2023" 8 | ], 9 | "module": "ESNext", 10 | "skipLibCheck": true, 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": [ 25 | "vite.config.ts" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, "src/index.ts"), 11 | name: "OvenplayerReact", 12 | fileName: (format) => `ovenplayer-react.${format}.js`, 13 | }, 14 | rollupOptions: { 15 | external: ["react", "react-dom", "ovenplayer"], 16 | output: { 17 | globals: { 18 | react: "React", 19 | "react-dom": "ReactDOM", 20 | ovenplayer: "OvenPlayer", 21 | }, 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/vue3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist-ssr 13 | coverage 14 | *.local 15 | 16 | /cypress/videos/ 17 | /cypress/screenshots/ 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | .idea 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /packages/vue3/.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/** 3 | !src/** -------------------------------------------------------------------------------- /packages/vue3/README.md: -------------------------------------------------------------------------------- 1 | # OvenPlayer-Vue3 2 | 3 | Reusable OvenPlayer component for Vue.js -------------------------------------------------------------------------------- /packages/vue3/dist/ovenplayer-vue3.es.js: -------------------------------------------------------------------------------- 1 | import { defineComponent as h, ref as v, onMounted as c, onUnmounted as i, watch as g, openBlock as C, createElementBlock as m } from "vue"; 2 | import y from "ovenplayer"; 3 | const k = /* @__PURE__ */ h({ 4 | __name: "OvenPlayer", 5 | props: { 6 | config: {} 7 | }, 8 | emits: ["ready", "metaChanged", "stateChanged", "resized", "playbackRateChanged", "seek", "time", "bufferChanged", "mute", "volumeChanged", "playlistChanged", "sourceChanged", "qualityLevelChanged", "cueChanged", "timeDisplayModeChanged", "adChanged", "adTime", "adComplete", "fullscreenChanged", "clicked", "allPlaylistEnded", "hlsPrepared", "hlsDestroyed", "dashPrepared", "dashDestroyed", "destroy", "error"], 9 | setup(t, { expose: s, emit: u }) { 10 | const a = v(), o = "ovenplayer-" + Math.random().toString().replace(".", ""), r = t, n = u, d = () => { 11 | a.value = y.create(o, r.config), a.value.on("ready", () => n("ready")), a.value.on("metaChanged", (e) => n("metaChanged", e)), a.value.on("stateChanged", (e) => n("stateChanged", e)), a.value.on("resized", (e) => n("resized", e)), a.value.on("playbackRateChanged", (e) => n("playbackRateChanged", e)), a.value.on("seek", (e) => n("seek", e)), a.value.on("time", (e) => n("time", e)), a.value.on("bufferChanged", (e) => n("bufferChanged", e)), a.value.on("mute", (e) => n("mute", e)), a.value.on("volumeChanged", (e) => n("volumeChanged", e)), a.value.on("playlistChanged", (e) => n("playlistChanged", e)), a.value.on("sourceChanged", (e) => n("sourceChanged", e)), a.value.on("qualityLevelChanged", (e) => n("qualityLevelChanged", e)), a.value.on("cueChanged", (e) => n("cueChanged", e)), a.value.on("timeDisplayModeChanged", (e) => n("timeDisplayModeChanged", e)), a.value.on("adChanged", (e) => n("adChanged", e)), a.value.on("adTime", (e) => n("adTime", e)), a.value.on("adComplete", () => n("adComplete")), a.value.on("fullscreenChanged", (e) => n("fullscreenChanged", e)), a.value.on("clicked", (e) => n("clicked", e)), a.value.on("allPlaylistEnded", () => n("allPlaylistEnded")), a.value.on("hlsPrepared", (e) => n("hlsPrepared", e)), a.value.on("hlsDestroyed", () => n("hlsDestroyed")), a.value.on("dashPrepared", (e) => n("dashPrepared", e)), a.value.on("dashDestroyed", () => n("dashDestroyed")), a.value.on("destroy", () => n("destroy")), a.value.on("error", (e) => n("error", e)); 12 | }, l = () => { 13 | if (a.value) { 14 | try { 15 | a.value.remove(); 16 | } catch { 17 | } 18 | a.value = void 0; 19 | } 20 | }; 21 | return s({ 22 | createPlayer: d, 23 | removePlayer: l, 24 | playerInstance: a 25 | }), c(d), i(l), g( 26 | () => r.config, 27 | () => { 28 | l(), d(); 29 | }, 30 | { deep: !0 } 31 | ), (e, p) => (C(), m("div", { id: o })); 32 | } 33 | }); 34 | export { 35 | k as default 36 | }; 37 | -------------------------------------------------------------------------------- /packages/vue3/dist/ovenplayer-vue3.umd.js: -------------------------------------------------------------------------------- 1 | (function(d,l){typeof exports=="object"&&typeof module<"u"?module.exports=l(require("vue"),require("ovenplayer")):typeof define=="function"&&define.amd?define(["vue","ovenplayer"],l):(d=typeof globalThis<"u"?globalThis:d||self,d["ovenplayer-vue3"]=l(d.Vue,d.OvenPlayer))})(this,function(d,l){"use strict";return d.defineComponent({__name:"OvenPlayer",props:{config:{}},emits:["ready","metaChanged","stateChanged","resized","playbackRateChanged","seek","time","bufferChanged","mute","volumeChanged","playlistChanged","sourceChanged","qualityLevelChanged","cueChanged","timeDisplayModeChanged","adChanged","adTime","adComplete","fullscreenChanged","clicked","allPlaylistEnded","hlsPrepared","hlsDestroyed","dashPrepared","dashDestroyed","destroy","error"],setup(u,{expose:h,emit:i}){const a=d.ref(),r="ovenplayer-"+Math.random().toString().replace(".",""),s=u,n=i,o=()=>{a.value=l.create(r,s.config),a.value.on("ready",()=>n("ready")),a.value.on("metaChanged",e=>n("metaChanged",e)),a.value.on("stateChanged",e=>n("stateChanged",e)),a.value.on("resized",e=>n("resized",e)),a.value.on("playbackRateChanged",e=>n("playbackRateChanged",e)),a.value.on("seek",e=>n("seek",e)),a.value.on("time",e=>n("time",e)),a.value.on("bufferChanged",e=>n("bufferChanged",e)),a.value.on("mute",e=>n("mute",e)),a.value.on("volumeChanged",e=>n("volumeChanged",e)),a.value.on("playlistChanged",e=>n("playlistChanged",e)),a.value.on("sourceChanged",e=>n("sourceChanged",e)),a.value.on("qualityLevelChanged",e=>n("qualityLevelChanged",e)),a.value.on("cueChanged",e=>n("cueChanged",e)),a.value.on("timeDisplayModeChanged",e=>n("timeDisplayModeChanged",e)),a.value.on("adChanged",e=>n("adChanged",e)),a.value.on("adTime",e=>n("adTime",e)),a.value.on("adComplete",()=>n("adComplete")),a.value.on("fullscreenChanged",e=>n("fullscreenChanged",e)),a.value.on("clicked",e=>n("clicked",e)),a.value.on("allPlaylistEnded",()=>n("allPlaylistEnded")),a.value.on("hlsPrepared",e=>n("hlsPrepared",e)),a.value.on("hlsDestroyed",()=>n("hlsDestroyed")),a.value.on("dashPrepared",e=>n("dashPrepared",e)),a.value.on("dashDestroyed",()=>n("dashDestroyed")),a.value.on("destroy",()=>n("destroy")),a.value.on("error",e=>n("error",e))},t=()=>{if(a.value){try{a.value.remove()}catch{}a.value=void 0}};return h({createPlayer:o,removePlayer:t,playerInstance:a}),d.onMounted(o),d.onUnmounted(t),d.watch(()=>s.config,()=>{t(),o()},{deep:!0}),(e,v)=>(d.openBlock(),d.createElementBlock("div",{id:r}))}})}); 2 | -------------------------------------------------------------------------------- /packages/vue3/dist/types/OvenPlayer.vue.d.ts: -------------------------------------------------------------------------------- 1 | import type { OvenPlayerConfig, OvenPlayerInstance } from 'ovenplayer'; 2 | declare const _default: import("vue").DefineComponent<{ 3 | config: { 4 | type: import("vue").PropType; 5 | required: true; 6 | }; 7 | }, { 8 | createPlayer: () => void; 9 | removePlayer: () => void; 10 | playerInstance: import("vue").Ref; 11 | }, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, { 12 | ready: () => void; 13 | metaChanged: (data: { 14 | duration: number; 15 | isP2P: boolean; 16 | type: string; 17 | }) => void; 18 | stateChanged: (data: { 19 | prevstate: import("ovenplayer").OvenPlayerState; 20 | newstate: import("ovenplayer").OvenPlayerState; 21 | }) => void; 22 | resized: (data: "large" | "medium" | "small" | "xsmall") => void; 23 | playbackRateChanged: (data: number) => void; 24 | seek: (data: { 25 | position: string; 26 | newstate: string; 27 | }) => void; 28 | time: (data: { 29 | duration: number; 30 | position: number; 31 | }) => void; 32 | bufferChanged: (data: { 33 | duration: number; 34 | position: number; 35 | buffer: number; 36 | }) => void; 37 | mute: (data: number) => void; 38 | volumeChanged: (data: number) => void; 39 | playlistChanged: (data: number) => void; 40 | sourceChanged: (data: number) => void; 41 | qualityLevelChanged: (data: { 42 | currentQuality: number; 43 | type: "request" | "render"; 44 | isAuto: boolean; 45 | }) => void; 46 | cueChanged: (data: VTTCue) => void; 47 | timeDisplayModeChanged: (data: boolean) => void; 48 | adChanged: (data: { 49 | isLinear: boolean; 50 | duration: number; 51 | skipTimeOffset: number; 52 | }) => void; 53 | adTime: (data: { 54 | isLinear: boolean; 55 | duration: number; 56 | skipTimeOffset: number; 57 | remaining: number; 58 | position: number; 59 | }) => void; 60 | adComplete: () => void; 61 | fullscreenChanged: (data: boolean) => void; 62 | clicked: (data: Event) => void; 63 | allPlaylistEnded: () => void; 64 | hlsPrepared: (data: object) => void; 65 | hlsDestroyed: () => void; 66 | dashPrepared: (data: object) => void; 67 | dashDestroyed: () => void; 68 | destroy: () => void; 69 | error: (data: { 70 | code: number; 71 | error?: string | Error | undefined; 72 | message: string; 73 | reason: string; 74 | }) => void; 75 | }, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; 78 | required: true; 79 | }; 80 | }>> & { 81 | onReady?: (() => any) | undefined; 82 | onMetaChanged?: ((data: { 83 | duration: number; 84 | isP2P: boolean; 85 | type: string; 86 | }) => any) | undefined; 87 | onStateChanged?: ((data: { 88 | prevstate: import("ovenplayer").OvenPlayerState; 89 | newstate: import("ovenplayer").OvenPlayerState; 90 | }) => any) | undefined; 91 | onResized?: ((data: "large" | "medium" | "small" | "xsmall") => any) | undefined; 92 | onPlaybackRateChanged?: ((data: number) => any) | undefined; 93 | onSeek?: ((data: { 94 | position: string; 95 | newstate: string; 96 | }) => any) | undefined; 97 | onTime?: ((data: { 98 | duration: number; 99 | position: number; 100 | }) => any) | undefined; 101 | onBufferChanged?: ((data: { 102 | duration: number; 103 | position: number; 104 | buffer: number; 105 | }) => any) | undefined; 106 | onMute?: ((data: number) => any) | undefined; 107 | onVolumeChanged?: ((data: number) => any) | undefined; 108 | onPlaylistChanged?: ((data: number) => any) | undefined; 109 | onSourceChanged?: ((data: number) => any) | undefined; 110 | onQualityLevelChanged?: ((data: { 111 | currentQuality: number; 112 | type: "request" | "render"; 113 | isAuto: boolean; 114 | }) => any) | undefined; 115 | onCueChanged?: ((data: VTTCue) => any) | undefined; 116 | onTimeDisplayModeChanged?: ((data: boolean) => any) | undefined; 117 | onAdChanged?: ((data: { 118 | isLinear: boolean; 119 | duration: number; 120 | skipTimeOffset: number; 121 | }) => any) | undefined; 122 | onAdTime?: ((data: { 123 | isLinear: boolean; 124 | duration: number; 125 | skipTimeOffset: number; 126 | remaining: number; 127 | position: number; 128 | }) => any) | undefined; 129 | onAdComplete?: (() => any) | undefined; 130 | onFullscreenChanged?: ((data: boolean) => any) | undefined; 131 | onClicked?: ((data: Event) => any) | undefined; 132 | onAllPlaylistEnded?: (() => any) | undefined; 133 | onHlsPrepared?: ((data: object) => any) | undefined; 134 | onHlsDestroyed?: (() => any) | undefined; 135 | onDashPrepared?: ((data: object) => any) | undefined; 136 | onDashDestroyed?: (() => any) | undefined; 137 | onDestroy?: (() => any) | undefined; 138 | onError?: ((data: { 139 | code: number; 140 | error?: string | Error | undefined; 141 | message: string; 142 | reason: string; 143 | }) => any) | undefined; 144 | }, {}, {}>; 145 | export default _default; 146 | -------------------------------------------------------------------------------- /packages/vue3/dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import OvenPlayerVue3 from './OvenPlayer.vue'; 2 | export default OvenPlayerVue3; 3 | -------------------------------------------------------------------------------- /packages/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ovenplayer-vue3", 3 | "version": "1.0.0-beta.3", 4 | "type": "module", 5 | "main": "dist/ovenplayer-vue3.umd.js", 6 | "module": "dist/ovenplayer-vue3.es.js", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vue-tsc --noEmit && vite build && vue-tsc --declaration --emitDeclarationOnly", 10 | "preview": "vite preview" 11 | }, 12 | "exports": { 13 | ".": { 14 | "types": "./dist/types/index.d.ts", 15 | "import": "./dist/ovenplayer-vue3.es.js", 16 | "require": "./dist/ovenplayer-vue3.umd.js" 17 | } 18 | }, 19 | "peerDependencies": { 20 | "vue": "^3.3.6" 21 | }, 22 | "dependencies": { 23 | "ovenplayer": "^0.10.31" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^18.18.6", 27 | "@types/ovenplayer": "^0.10.8", 28 | "@vitejs/plugin-vue": "^5.2.2", 29 | "typescript": "^5.2.2", 30 | "vite": "^6.2.2", 31 | "vue-tsc": "^2.1.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vue3/src/index.ts: -------------------------------------------------------------------------------- 1 | import OvenPlayerVue3 from './OvenPlayer.vue'; 2 | export default OvenPlayerVue3; -------------------------------------------------------------------------------- /packages/vue3/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | // "noEmit": true, 15 | // "emitDeclarationOnly": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | "declaration": true, 25 | "declarationDir": "./dist/types" 26 | }, 27 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/vue3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, 'src/index.ts'), 11 | name: 'ovenplayer-vue3', 12 | fileName: (format) => `ovenplayer-vue3.${format}.js` 13 | }, 14 | rollupOptions: { 15 | external: ['ovenplayer', 'vue'], 16 | output: { 17 | globals: { 18 | ovenplayer: 'OvenPlayer', 19 | vue: 'Vue' 20 | } 21 | } 22 | } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/assets/fonts/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/fontello.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/fontello.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/fontello.woff -------------------------------------------------------------------------------- /src/assets/fonts/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/fontello.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/seek-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/seek-icons.eot -------------------------------------------------------------------------------- /src/assets/fonts/seek-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2020 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/fonts/seek-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/seek-icons.ttf -------------------------------------------------------------------------------- /src/assets/fonts/seek-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/seek-icons.woff -------------------------------------------------------------------------------- /src/assets/fonts/seek-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/fonts/seek-icons.woff2 -------------------------------------------------------------------------------- /src/assets/images/ic-player-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-frame-jump.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/images/ic-player-frame-jump.eot -------------------------------------------------------------------------------- /src/assets/images/ic-player-frame-jump.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-frame-jump.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/images/ic-player-frame-jump.ttf -------------------------------------------------------------------------------- /src/assets/images/ic-player-frame-jump.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirenSoft/OvenPlayer/3f233b60d01f4ae4c966b88160a73b1b468e0650/src/assets/images/ic-player-frame-jump.woff -------------------------------------------------------------------------------- /src/assets/images/ic-player-fullscreen-compress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-fullscreen-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-non-thumb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-play-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-re-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-stop-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-volume-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-volume-mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/ic-player-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-fullscreen-compress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-fullscreen-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-playlist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-volume-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-volume-mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/images/small/ic-player-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/js/api/ApiExpansions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 8. 24.. 3 | */ 4 | 5 | export const ApiRtmpExpansion = function(currentProvider){ 6 | return { 7 | externalCallbackCreep : (result) => { 8 | if(result.name && result.data){ 9 | return currentProvider.triggerEventFromExternal(result.name, result.data); 10 | }else{ 11 | return null; 12 | } 13 | } 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/js/api/EventEmitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 3.. 3 | */ 4 | 5 | /** 6 | * @brief This module provide custom events. 7 | * @param object An object that requires custom events. 8 | * 9 | * */ 10 | 11 | const EventEmitter = function(object){ 12 | let that = object; 13 | let _events =[]; 14 | 15 | const triggerEvents = function(events, args, context){ 16 | let i = 0; 17 | let length = events.length; 18 | for(i = 0; i < length; i ++){ 19 | let event = events[i]; 20 | event.listener.apply( ( event.context || context ), args); 21 | } 22 | }; 23 | 24 | that.on = function(name, listener, context){ 25 | (_events[name] || (_events[name]=[]) ).push({ listener: listener , context : context}); 26 | return that; 27 | }; 28 | that.trigger = function(name){ 29 | if(!_events){ 30 | return false; 31 | } 32 | const args = [].slice.call(arguments, 1); 33 | const events = _events[name]; 34 | const allEvents = _events.all; 35 | 36 | if(events){ 37 | triggerEvents(events, args, that); 38 | } 39 | if(allEvents){ 40 | triggerEvents(allEvents, arguments, that); 41 | } 42 | }; 43 | that.off = function(name, listener, context){ 44 | if(!_events){ 45 | return false; 46 | } 47 | 48 | if (!name && !listener && !context) { 49 | _events = []; 50 | return that; 51 | } 52 | 53 | const names = name ? [name] : Object.keys(_events); 54 | 55 | for (let i = 0, l = names.length; i < l; i++) { 56 | name = names[i]; 57 | const events = _events[name]; 58 | if (events) { 59 | const retain = _events[name] = []; 60 | if (listener || context) { 61 | for (let j = 0, k = events.length; j < k; j++) { 62 | const event = events[j]; 63 | if ((listener && listener !== event.listener && listener !== event.listener.listener && listener !== event.listener._listener) 64 | ||(context && context !== event.context) 65 | ) { 66 | retain.push(event); 67 | } 68 | } 69 | } 70 | if (!retain.length) { 71 | delete _events[name]; 72 | } 73 | } 74 | } 75 | return that; 76 | }; 77 | that.once = function(name, listener, context){ 78 | let count = 0; 79 | const onceCallback = function() { 80 | if (count++) { 81 | return; 82 | } 83 | that.off(name, onceCallback); 84 | listener.apply(that, arguments); 85 | }; 86 | onceCallback._listener = listener; 87 | return that.on(name, onceCallback, context); 88 | }; 89 | 90 | return that; 91 | } 92 | 93 | export default EventEmitter; 94 | -------------------------------------------------------------------------------- /src/js/api/ads/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 27/06/2019. 3 | */ 4 | export const TEMP_VIDEO_URL = "data:video/mp4;base64, AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf//rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAAAAAEAAAABAAAACQAAAAIAAAABAAAACgAAAAEAAAABAAAACwAAAAIAAAABAAAADQAAAAEAAAABAAAADgAAAAIAAAABAAAADwAAAAEAAAABAAAAEAAAAAIAAAABAAAAEQAAAAEAAAABAAAAEgAAAAIAAAABAAAAFAAAAAEAAAABAAAAFQAAAAIAAAABAAAAFgAAAAEAAAABAAAAFwAAAAIAAAABAAAAGAAAAAEAAAABAAAAGQAAAAIAAAABAAAAGgAAAAEAAAABAAAAGwAAAAIAAAABAAAAHQAAAAEAAAABAAAAHgAAAAIAAAABAAAAHwAAAAQAAAABAAAA4HN0c3oAAAAAAAAAAAAAADMAAAAaAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAAAJAAAACQAAAAkAAACMc3RjbwAAAAAAAAAfAAAALAAAA1UAAANyAAADhgAAA6IAAAO+AAAD0QAAA+0AAAQAAAAEHAAABC8AAARLAAAEZwAABHoAAASWAAAEqQAABMUAAATYAAAE9AAABRAAAAUjAAAFPwAABVIAAAVuAAAFgQAABZ0AAAWwAAAFzAAABegAAAX7AAAGFwAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTUuMzMuMTAw"; 5 | -------------------------------------------------------------------------------- /src/js/api/caption/Loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 4.. 3 | */ 4 | import SrtParser from "api/caption/parser/SrtParser"; 5 | import WebVTT from 'api/caption/parser/VttParser' 6 | import VTTCue from 'utils/captions/vttCue' 7 | import SmiParser from 'api/caption/parser/SmiParser' 8 | 9 | const Loader = function () { 10 | const that = {}; 11 | 12 | const convertToVTTCues = function (cues) { 13 | return cues.map(cue => new VTTCue(cue.start, cue.end, cue.text)); 14 | }; 15 | 16 | that.load = (track, language, successCallback, errorCallback) => { 17 | 18 | fetch(track.file).then(function (response) { 19 | if (response.ok) { 20 | 21 | response.text().then(function (body) { 22 | let cues = []; 23 | let vttCues = []; 24 | 25 | if (body.indexOf('WEBVTT') >= 0) { 26 | OvenPlayerConsole.log("WEBVTT LOADED"); 27 | let parser = new WebVTT.Parser(window, WebVTT.StringDecoder()); 28 | vttCues = []; 29 | parser.oncue = function (cue) { 30 | vttCues.push(cue); 31 | }; 32 | parser.onflush = function () { 33 | //delete track.xhr; 34 | successCallback(vttCues); 35 | }; 36 | // Parse calls onflush internally 37 | parser.parse(body); 38 | parser.flush(); 39 | } else if (body.indexOf('SAMI') >= 0) { 40 | OvenPlayerConsole.log("SAMI LOADED"); 41 | let parsedData = SmiParser(body, {fixedLang: language}); 42 | vttCues = convertToVTTCues(parsedData.result); 43 | successCallback(vttCues); 44 | 45 | } else { 46 | OvenPlayerConsole.log("SRT LOADED"); 47 | cues = SrtParser(body); 48 | vttCues = convertToVTTCues(cues); 49 | successCallback(vttCues); 50 | } 51 | }).catch(function (e) { 52 | errorCallback(e); 53 | }); 54 | 55 | } else { 56 | errorCallback(response.status); 57 | } 58 | }).catch(function (e) { 59 | errorCallback(e); 60 | }); 61 | 62 | }; 63 | 64 | return that; 65 | }; 66 | 67 | export default Loader; 68 | -------------------------------------------------------------------------------- /src/js/api/caption/Manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 5. 17.. 3 | */ 4 | import CaptionLoader from 'api/caption/Loader'; 5 | import {READY, ERRORS, ERROR, PLAYER_CAPTION_ERROR, CONTENT_META, CONTENT_TIME, CONTENT_CAPTION_CUE_CHANGED, CONTENT_CAPTION_CHANGED} from "api/constants"; 6 | import _ from "utils/underscore"; 7 | 8 | const isSupport = function(kind){ 9 | return kind === 'subtitles' || kind === 'captions'; 10 | }; 11 | 12 | const Manager = function(api, playlistIndex){ 13 | 14 | const that = {}; 15 | let captionList = []; 16 | let currentCaptionIndex = -1; 17 | 18 | let captionLoader = CaptionLoader(); 19 | let isFisrtLoad = true; 20 | let isShowing = false; 21 | 22 | OvenPlayerConsole.log("Caption Manager >> ", playlistIndex); 23 | 24 | 25 | let bindTrack = function(track, vttCues){ 26 | track.data = vttCues || []; 27 | track.name = track.label || track.name || track.language; 28 | track.id = (function(track, tracksCount) { 29 | var trackId; 30 | var prefix = track.kind || 'cc'; 31 | if (track.default || track.defaulttrack) { 32 | trackId = 'default'; 33 | 34 | } else { 35 | trackId = track.id || (prefix + tracksCount); 36 | } 37 | if(isFisrtLoad){ 38 | //This execute only on. and then use flushCaptionList(lastCaptionIndex); 39 | changeCurrentCaption(captionList.length||0); 40 | isFisrtLoad = false; 41 | 42 | } 43 | return trackId; 44 | })(track, captionList.length); 45 | 46 | captionList.push(track); 47 | return track.id; 48 | }; 49 | let changeCurrentCaption = function(index){ 50 | currentCaptionIndex = index; 51 | api.trigger(CONTENT_CAPTION_CHANGED, currentCaptionIndex); 52 | }; 53 | if(api.getConfig().playlist && api.getConfig().playlist.length > 0){ 54 | let playlist = api.getConfig().playlist[playlistIndex]; 55 | 56 | if(playlist && playlist.tracks && playlist.tracks.length > 0){ 57 | for(let i = 0; i < playlist.tracks.length; i ++){ 58 | const track = playlist.tracks[i]; 59 | 60 | if(isSupport(track.kind) && ! _.findWhere(track, {file : track.file})){ 61 | //that.flushCaptionList(currentCaptionIndex); 62 | 63 | captionLoader.load(track, track.lang, function(vttCues){ 64 | if(vttCues && vttCues.length > 0){ 65 | let captionId = bindTrack(track, vttCues); 66 | } 67 | }, function(error){ 68 | let tempError = ERRORS.codes[PLAYER_CAPTION_ERROR]; 69 | tempError.error = error; 70 | api.trigger(ERROR, tempError); 71 | }); 72 | } 73 | } 74 | 75 | } 76 | } 77 | 78 | api.on(CONTENT_TIME, function(meta){ 79 | let position = meta.position; 80 | if(currentCaptionIndex > -1 && captionList[currentCaptionIndex]){ 81 | let currentCues = _.filter(captionList[currentCaptionIndex].data, function (cue) { 82 | return position >= (cue.startTime) && ( (!cue.endTime || position) <= cue.endTime); 83 | }); 84 | if(currentCues && currentCues.length > 0){ 85 | api.trigger(CONTENT_CAPTION_CUE_CHANGED, currentCues[0]); 86 | } 87 | } 88 | 89 | }); 90 | that.flushCaptionList = (lastCaptionIndex) =>{ 91 | captionList = []; 92 | changeCurrentCaption(lastCaptionIndex); 93 | //currentCaptionIndex = lastCaptionIndex; 94 | }; 95 | that.getCaptionList = () =>{ 96 | return captionList||[]; 97 | }; 98 | that.getCurrentCaption = () =>{ 99 | return currentCaptionIndex; 100 | }; 101 | that.setCurrentCaption = (_index) =>{ 102 | if(_index > -2 && _index < captionList.length){ 103 | changeCurrentCaption(_index); 104 | }else{ 105 | return null; 106 | } 107 | }; 108 | that.addCaption = (track) =>{ 109 | if(isSupport(track.kind) && ! _.findWhere(captionLoader, {file : track.file})){ 110 | captionLoader.load(track, function(vttCues){ 111 | if(vttCues && vttCues.length > 0){ 112 | bindTrack(track, vttCues); 113 | } 114 | }, function(error){ 115 | let tempError = errors[PLAYER_CAPTION_ERROR]; 116 | tempError.error = error; 117 | api.trigger(ERROR, tempError); 118 | }); 119 | } 120 | }; 121 | that.removeCaption = (index) => { 122 | if(index > -1 && index < captionList.length){ 123 | captionList.splice(index, 1); 124 | return captionList; 125 | }else{ 126 | return null; 127 | } 128 | }; 129 | that.destroy = () => { 130 | captionList = []; 131 | captionLoader = null; 132 | api.off(CONTENT_TIME, null, that); 133 | }; 134 | 135 | return that; 136 | }; 137 | 138 | 139 | 140 | 141 | export default Manager; 142 | -------------------------------------------------------------------------------- /src/js/api/caption/parser/SrtParser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 5. 29.. 3 | */ 4 | import { hmsToSecond, trim } from "utils/strings" 5 | 6 | function _entry(data) { 7 | var entry = {}; 8 | var array = data.split('\r\n'); 9 | if (array.length === 1) { 10 | array = data.split('\n'); 11 | } 12 | var idx = 1; 13 | if (array[0].indexOf(' --> ') > 0) { 14 | idx = 0; 15 | } 16 | if (array.length > idx + 1 && array[idx + 1]) { 17 | // This line contains the start and end. 18 | var line = array[idx]; 19 | var index = line.indexOf(' --> '); 20 | if (index > 0) { 21 | entry.start = hmsToSecond(line.substr(0, index)); 22 | entry.end = hmsToSecond(line.substr(index + 5)); 23 | entry.text = array.slice(idx + 1).join('\r\n'); 24 | } 25 | } 26 | return entry; 27 | 28 | } 29 | 30 | const SrtParser = function(data) { 31 | var captions = []; 32 | 33 | data = trim(data); 34 | 35 | var list = data.split('\r\n\r\n'); 36 | if (list.length === 1) { 37 | list = data.split('\n\n'); 38 | } 39 | 40 | 41 | 42 | for (var i = 0; i < list.length; i++) { 43 | if (list[i] === 'WEBVTT') { 44 | continue; 45 | } 46 | var entry = _entry(list[i]); 47 | if (entry.text) { 48 | captions.push(entry); 49 | } 50 | } 51 | 52 | return captions; 53 | } 54 | 55 | 56 | 57 | export default SrtParser; -------------------------------------------------------------------------------- /src/js/api/media/Manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @brief 미디어 엘리먼트를 관리하는 객체. 현재는 하는 일이 많지 않다. 3 | * @param {element} container dom element 4 | * 5 | * */ 6 | import {getBrowser} from "utils/browser"; 7 | import {PROVIDER_DASH, PROVIDER_HLS, PROVIDER_WEBRTC, PROVIDER_HTML5, PROVIDER_RTMP} from "api/constants"; 8 | import LA$ from "utils/likeA$.js"; 9 | import {version} from 'version'; 10 | 11 | const Manager = function(container, browserInfo){ 12 | const that = {}; 13 | let $container = LA$(container); 14 | let videoElement = ""; 15 | 16 | OvenPlayerConsole.log("MediaManager loaded. browser : ", browserInfo ); 17 | 18 | const createHtmlVideo = function(isLoop, isAutoStart){ 19 | 20 | videoElement = document.createElement('video'); 21 | videoElement.setAttribute('preload', 'auto'); 22 | videoElement.setAttribute('disableremoteplayback', ''); 23 | videoElement.setAttribute('webkit-playsinline', 'true'); 24 | videoElement.setAttribute('playsinline', 'true'); 25 | if(isLoop){ 26 | videoElement.setAttribute('loop', ''); 27 | } 28 | if(isAutoStart) { 29 | videoElement.setAttribute('autoplay', ''); 30 | } 31 | $container.append(videoElement); 32 | 33 | return videoElement; 34 | }; 35 | 36 | that.createMedia = (providerName , playerConfig) => { 37 | // if(videoElement){ 38 | // // that.empty(); 39 | // //reuse video element. 40 | // //because playlist is auto next playing. 41 | // //Only same video element does not require User Interaction Error. 42 | // return videoElement; 43 | // }else{ 44 | // return createHtmlVideo(playerConfig.isLoop(), playerConfig.isAutoStart()); 45 | // } 46 | that.empty(); 47 | return createHtmlVideo(playerConfig.isLoop(), playerConfig.isAutoStart()); 48 | } 49 | 50 | that.createAdContainer = () => { 51 | let adContainer = document.createElement('div'); 52 | adContainer.setAttribute('class', 'op-ads'); 53 | $container.append(adContainer); 54 | 55 | return adContainer; 56 | }; 57 | 58 | 59 | that.empty = () =>{ 60 | OvenPlayerConsole.log("MediaManager removeElement()"); 61 | $container.removeChild(videoElement); 62 | videoElement = null; 63 | }; 64 | 65 | that.destroy = () =>{ 66 | $container.removeChild(); 67 | $container = null; 68 | if(videoElement!=null && videoElement!=='') { 69 | videoElement.srcObject = null; 70 | } 71 | videoElement = null; 72 | }; 73 | 74 | return that; 75 | }; 76 | 77 | export default Manager; 78 | -------------------------------------------------------------------------------- /src/js/api/provider/Controller.js: -------------------------------------------------------------------------------- 1 | import SupportChecker from "api/SupportChecker"; 2 | import HTML5 from "api/provider/html5/providers/Html5"; 3 | import WebRTC from "api/provider/html5/providers/WebRTC"; 4 | import Dash from "api/provider/html5/providers/Dash"; 5 | import Hls from "api/provider/html5/providers/Hls"; 6 | 7 | import { 8 | PROVIDER_HTML5, PROVIDER_WEBRTC, PROVIDER_DASH, PROVIDER_HLS, PROVIDER_RTMP, ERRORS, INIT_UNSUPPORT_ERROR 9 | } from "api/constants"; 10 | 11 | /** 12 | * @brief This manages provider. 13 | * */ 14 | const Controller = function () { 15 | let supportChecker = SupportChecker(); 16 | const Providers = {}; 17 | 18 | const that = {}; 19 | OvenPlayerConsole.log("ProviderController loaded."); 20 | 21 | const registeProvider = (name, provider) => { 22 | if (Providers[name]) { 23 | return; 24 | } 25 | OvenPlayerConsole.log("ProviderController _registerProvider() ", name); 26 | Providers[name] = provider; 27 | }; 28 | 29 | const ProviderLoader = { 30 | html5: function () { 31 | 32 | const provider = HTML5; 33 | registeProvider(PROVIDER_HTML5, provider); 34 | return {name: PROVIDER_HTML5, provider: provider}; 35 | }, 36 | webrtc: function () { 37 | 38 | const provider = WebRTC; 39 | registeProvider(PROVIDER_WEBRTC, provider); 40 | return {name: PROVIDER_WEBRTC, provider: provider}; 41 | }, 42 | dash: function () { 43 | 44 | const provider = Dash; 45 | registeProvider(PROVIDER_DASH, provider); 46 | return {name: PROVIDER_DASH, provider: provider}; 47 | }, 48 | hls: function () { 49 | 50 | const provider = Hls; 51 | registeProvider(PROVIDER_HLS, provider); 52 | return {name: PROVIDER_HLS, provider: provider}; 53 | } 54 | }; 55 | 56 | 57 | that.loadProviders = (playlistItem) => { 58 | const supportedProviderNames = supportChecker.findProviderNamesByPlaylist(playlistItem); 59 | OvenPlayerConsole.log("ProviderController loadProviders() ", supportedProviderNames); 60 | if (!supportedProviderNames) { 61 | return Promise.reject(ERRORS.codes[INIT_UNSUPPORT_ERROR]); 62 | } else { 63 | return Promise.all( 64 | supportedProviderNames.filter(function (providerName) { 65 | return !!ProviderLoader[providerName]; 66 | }).map(function (providerName) { 67 | return ProviderLoader[providerName](); 68 | }) 69 | ); 70 | } 71 | 72 | }; 73 | 74 | that.findByName = (name) => { 75 | OvenPlayerConsole.log("ProviderController findByName() ", name); 76 | return Providers[name]; 77 | }; 78 | 79 | that.getProviderBySource = (source) => { 80 | const supportedProviderName = supportChecker.findProviderNameBySource(source); 81 | OvenPlayerConsole.log("ProviderController getProviderBySource() ", supportedProviderName); 82 | return that.findByName(supportedProviderName); 83 | }; 84 | 85 | that.isSameProvider = (currentSource, newSource) => { 86 | OvenPlayerConsole.log("ProviderController isSameProvider() ", supportChecker.findProviderNameBySource(currentSource), supportChecker.findProviderNameBySource(newSource)); 87 | return supportChecker.findProviderNameBySource(currentSource) === supportChecker.findProviderNameBySource(newSource); 88 | }; 89 | 90 | return that; 91 | }; 92 | 93 | export default Controller; 94 | -------------------------------------------------------------------------------- /src/js/api/provider/html5/providers/Html5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 8. 24.. 3 | */ 4 | import Provider from "api/provider/html5/Provider"; 5 | import { errorTrigger } from "api/provider/utils"; 6 | import { PROVIDER_HTML5, STATE_IDLE } from "api/constants"; 7 | import { 8 | arrayToString, 9 | base64DecodeUint8Array, 10 | base64EncodeUint8Array, 11 | stringToUint16Array, 12 | concatInitDataIdAndCertificate 13 | } from "utils/eme"; 14 | import { ERRORS, INIT_DRM_FAIL } from "api/constants"; 15 | 16 | /** 17 | * @brief html5 provider extended core. 18 | * @param container player element. 19 | * @param playerConfig config. 20 | * */ 21 | 22 | const Html5 = function (element, playerConfig, adTagUrl) { 23 | 24 | let spec = { 25 | name: PROVIDER_HTML5, 26 | element: element, 27 | mse: null, 28 | listener: null, 29 | isLoaded: false, 30 | canSeek: false, 31 | isLive: false, 32 | seeking: false, 33 | state: STATE_IDLE, 34 | buffer: 0, 35 | framerate: 0, 36 | currentQuality: -1, 37 | qualityLevels: [], 38 | currentAudioTrack: -1, 39 | audioTracks: [], 40 | currentSource: -1, 41 | sources: [], 42 | adTagUrl: adTagUrl 43 | }; 44 | 45 | let that = Provider(spec, playerConfig, null); 46 | let superDestroy_func = that.super('destroy'); 47 | 48 | OvenPlayerConsole.log("HTML5 PROVIDER LOADED."); 49 | 50 | 51 | let cert = null; 52 | 53 | function loadCertificate(serverCertificatePath) { 54 | 55 | return new Promise(function (resolve, reject) { 56 | 57 | fetch(serverCertificatePath).then(function (response) { 58 | 59 | response.text().then(function (text) { 60 | 61 | resolve(base64DecodeUint8Array(text)); 62 | }).catch(function (err) { 63 | reject(err); 64 | }); 65 | 66 | }).catch(function (err) { 67 | reject(err); 68 | }); 69 | }); 70 | } 71 | 72 | function getLicense(keyMessage) { 73 | 74 | return new Promise(function (resolve, reject) { 75 | 76 | const licenseServerUrl = playerConfig.getConfig().hlsConfig.drmSystems['com.apple.fps'].licenseUrl; 77 | const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }); 78 | 79 | if (playerConfig.getConfig().licenseCustomHeader) { 80 | headers.append(playerConfig.getConfig().licenseCustomHeader.key, playerConfig.getConfig().licenseCustomHeader.value); 81 | } 82 | 83 | fetch(licenseServerUrl, { 84 | method: 'POST', 85 | headers: headers, 86 | body: "spc=" + base64EncodeUint8Array(keyMessage), 87 | }).then(function (response) { 88 | 89 | response.text().then(function (text) { 90 | resolve(base64DecodeUint8Array(text)); 91 | }).catch(function (err) { 92 | reject(err); 93 | }); 94 | }).catch(function (err) { 95 | reject(err); 96 | }); 97 | }); 98 | } 99 | 100 | function addKey(initData, contentId) { 101 | 102 | return new Promise(function (resolve, reject) { 103 | 104 | if (!element.webkitKeys) { 105 | try { 106 | element.webkitSetMediaKeys(new window.WebKitMediaKeys('com.apple.fps.1_0')); 107 | } catch (err) { 108 | reject(err); 109 | return; 110 | } 111 | } 112 | 113 | let keySession; 114 | 115 | try { 116 | keySession = element.webkitKeys.createSession('video/mp4', concatInitDataIdAndCertificate({ 117 | id: contentId, 118 | initData: initData, 119 | cert: cert 120 | })); 121 | } catch (err) { 122 | reject(err); 123 | return; 124 | } 125 | 126 | keySession.contentId = contentId; 127 | 128 | keySession.addEventListener('webkitkeymessage', function (event) { 129 | 130 | getLicense(event.message).then(function (license) { 131 | 132 | keySession.update(new Uint8Array(license)); 133 | resolve(); 134 | }).catch(function (err) { 135 | reject(err); 136 | return; 137 | }); 138 | }); 139 | 140 | keySession.addEventListener('webkitkeyadded', function () { 141 | 142 | }); 143 | 144 | keySession.addEventListener('webkitkeyerror', function () { 145 | 146 | var err = keySession.error; 147 | reject(err); 148 | }); 149 | }); 150 | } 151 | 152 | function encrypted(event) { 153 | const initDataType = event.initDataType; 154 | const initData = event.initData; 155 | const initDataString = arrayToString(initData); 156 | const contentId = initDataString.substring(initDataString.indexOf('skd://') + 6); 157 | addKey(initData, contentId).catch(function (err) { 158 | let tempError = ERRORS.codes[INIT_DRM_FAIL]; 159 | tempError.message = 'Could not add add key.'; 160 | errorTrigger(ERRORS.codes[INIT_DRM_FAIL], that); 161 | }); 162 | } 163 | 164 | function enableDrm() { 165 | 166 | return new Promise(function (resolve, reject) { 167 | const serverCertificatePath = playerConfig.getConfig().hlsConfig.drmSystems['com.apple.fps'].serverCertificateUrl; 168 | loadCertificate(serverCertificatePath).then(function (certificate) { 169 | cert = certificate; 170 | element.addEventListener('webkitneedkey', encrypted, { once: true }); 171 | }).catch(function (err) { 172 | reject(err); 173 | }); 174 | }); 175 | } 176 | 177 | if (playerConfig.getConfig().hlsConfig && playerConfig.getConfig().hlsConfig.emeEnabled) { 178 | 179 | enableDrm().catch(function (err) { 180 | let tempError = ERRORS.codes[INIT_DRM_FAIL]; 181 | tempError.message = 'Could not load drm certificate.'; 182 | errorTrigger(ERRORS.codes[INIT_DRM_FAIL], that); 183 | }); 184 | } 185 | 186 | that.destroy = () => { 187 | OvenPlayerConsole.log("HTML5 : PROVIDER DESTROYED."); 188 | 189 | superDestroy_func(); 190 | }; 191 | 192 | return that; 193 | 194 | }; 195 | 196 | export default Html5; 197 | -------------------------------------------------------------------------------- /src/js/api/provider/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 11. 12.. 3 | */ 4 | import {ERROR, STATE_ERROR} from "api/constants"; 5 | import _ from "utils/underscore"; 6 | 7 | export const extractVideoElement = function(elementOrMse) { 8 | if(_.isElement(elementOrMse)){ 9 | return elementOrMse; 10 | } 11 | if(elementOrMse.getVideoElement){ 12 | return elementOrMse.getVideoElement(); 13 | }else if(elementOrMse.media){ 14 | return elementOrMse.media; 15 | } 16 | return null; 17 | }; 18 | 19 | export const separateLive = function(mse) { 20 | //ToDo : You consider hlsjs. But not now because we don't support hlsjs. 21 | 22 | if(mse && mse.isDynamic){ 23 | return mse.isDynamic(); 24 | }else{ 25 | return false; 26 | } 27 | }; 28 | 29 | export const errorTrigger = function(error, provider){ 30 | if(provider){ 31 | provider.setState(STATE_ERROR); 32 | provider.pause(); 33 | provider.trigger(ERROR, error ); 34 | } 35 | 36 | }; 37 | 38 | export const pickCurrentSource = (sources, playerConfig) => { 39 | 40 | let sourceIndex = 0; 41 | 42 | if (sources) { 43 | 44 | if (playerConfig.getSourceIndex() === -1) { 45 | 46 | for (var i = 0; i < sources.length; i++) { 47 | if (sources[i].default) { 48 | sourceIndex = i; 49 | break; 50 | } 51 | } 52 | } else { 53 | 54 | sourceIndex = playerConfig.getSourceIndex(); 55 | } 56 | 57 | } 58 | 59 | return sourceIndex; 60 | } -------------------------------------------------------------------------------- /src/js/api/worker/RTCTransformWorker.worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by rock on 2025. 2 3 | */ 4 | 5 | const OVENMEDIAENGINE_SEI_METADATA_UUID = '464d4c475241494e434f4c4f55524201'; 6 | 7 | function removeEmulationPreventionBytes(data) { 8 | const rbsp = []; 9 | for (let i = 0; i < data.length; i++) { 10 | 11 | if (i > 2 && data[i - 2] === 0x00 && data[i - 1] === 0x00 && data[i] === 0x03) { 12 | continue; // skip 0x03 13 | } 14 | rbsp.push(data[i]); 15 | } 16 | return new Uint8Array(rbsp); 17 | } 18 | 19 | function parseSEIPayload(rbsp) { 20 | 21 | const messages = []; 22 | 23 | let i = 0; 24 | const rbspLength = rbsp[rbsp.length - 1] === 0x80 ? rbsp.length - 1 : rbsp.length; 25 | 26 | while (i < rbspLength) { 27 | 28 | let type = 0; 29 | while (rbsp[i] === 0xFF) { 30 | type += 255; 31 | i++; 32 | } 33 | type += rbsp[i++]; 34 | 35 | let size = 0; 36 | while (rbsp[i] === 0xFF) { 37 | size += 255; 38 | i++; 39 | } 40 | size += rbsp[i++]; 41 | 42 | const payload = rbsp.slice(i, i + size); 43 | i += size; 44 | 45 | messages.push({ type, size, payload }); 46 | 47 | return { type, size, payload }; 48 | } 49 | 50 | return messages; 51 | } 52 | 53 | function toHexString(byteArray, delimiter = '') { 54 | return Array.from(byteArray, byte => { 55 | return ('0' + (byte & 0xFF).toString(16)).slice(-2); 56 | }).join(delimiter); 57 | } 58 | 59 | function toHexArray(byteArray) { 60 | return Array.from(byteArray, byte => { 61 | return ('0' + (byte & 0xFF).toString(16)).slice(-2); 62 | }); 63 | } 64 | 65 | function toUUID(byteArray) { 66 | const hexString = toHexString(byteArray); 67 | return [ 68 | hexString.slice(0, 8), 69 | hexString.slice(8, 12), 70 | hexString.slice(12, 16), 71 | hexString.slice(16, 20), 72 | hexString.slice(20, 24), 73 | hexString.slice(24, 32) 74 | ].join('-'); 75 | } 76 | 77 | function toTimestamp(byteArray) { 78 | const hexString = toHexString(byteArray); 79 | return parseInt(hexString, 16); 80 | } 81 | 82 | function toAsciiString(byteArray) { 83 | return String.fromCharCode.apply(null, byteArray); 84 | } 85 | 86 | function findNalStartIndex(frameData, offset) { 87 | while (offset < frameData.byteLength - 4) { 88 | if ((frameData[offset] === 0x00 && frameData[offset + 1] === 0x00) 89 | && (frameData[offset + 2] === 0x01 || (frameData[offset + 2] === 0x00 && frameData[offset + 3] === 0x01))) { 90 | return offset; 91 | } else { 92 | offset += 1; 93 | } 94 | } 95 | return -1; 96 | } 97 | 98 | function getNalus(frameData) { 99 | 100 | let offset = 0; 101 | const headerSize = 1; 102 | const nalus = []; 103 | 104 | while (offset < frameData.byteLength - 4) { 105 | 106 | const startCodeIndex = findNalStartIndex(frameData, offset); 107 | 108 | if (startCodeIndex >= offset) { 109 | 110 | const startCodeLength = frameData[startCodeIndex + 2] === 0x01 ? 3 : 4; 111 | const nextStartCodeIndex = findNalStartIndex(frameData, startCodeIndex + startCodeLength + headerSize); 112 | 113 | if (nextStartCodeIndex > startCodeIndex) { 114 | 115 | nalus.push(frameData.subarray(startCodeIndex, nextStartCodeIndex)); 116 | offset = nextStartCodeIndex; 117 | } else { 118 | 119 | nalus.push(frameData.subarray(startCodeIndex)); 120 | break; 121 | } 122 | } else { 123 | break; 124 | } 125 | } 126 | return nalus; 127 | } 128 | 129 | function createReceiverTransform() { 130 | return new TransformStream({ 131 | start() { }, 132 | flush() { }, 133 | async transform(encodedFrame, controller) { 134 | 135 | const nalus = getNalus(new Uint8Array(encodedFrame.data)); 136 | 137 | nalus.forEach((nalu) => { 138 | 139 | const startCodeLength = nalu[2] === 0x01 ? 3 : 4; 140 | const headerCodeLength = 1; 141 | const nalHeader = nalu[startCodeLength]; 142 | const nalType = nalHeader & 0x1F; 143 | 144 | // NAL Type SEI 145 | if (nalType === 6) { 146 | 147 | const rbsp = removeEmulationPreventionBytes(nalu.subarray(startCodeLength + headerCodeLength)); 148 | 149 | const parsedSei = parseSEIPayload(rbsp); 150 | 151 | const eventData = { 152 | nalu: nalu, 153 | sei: parsedSei 154 | }; 155 | 156 | const uuid = toHexString(parsedSei.payload.subarray(0, 16)); 157 | 158 | if (uuid === OVENMEDIAENGINE_SEI_METADATA_UUID) { 159 | 160 | postMessage({ 161 | action: 'sei', data: { 162 | ...eventData, 163 | registered: true, 164 | uuid: toUUID(parsedSei.payload.subarray(0, 16)), 165 | timecode: toTimestamp(parsedSei.payload.subarray(16, 24)), 166 | userdata: parsedSei.payload.subarray(24) 167 | } 168 | }); 169 | } else { 170 | 171 | postMessage({ 172 | action: 'sei', data: { 173 | ...eventData, 174 | registered: false 175 | } 176 | }); 177 | } 178 | } 179 | }); 180 | 181 | controller.enqueue(encodedFrame); 182 | } 183 | }) 184 | } 185 | 186 | function setupPipe({ readable, writable }, transform) { 187 | readable 188 | .pipeThrough(transform) 189 | .pipeTo(writable) 190 | } 191 | 192 | addEventListener('rtctransform', (event) => { 193 | setupPipe(event.transformer, createReceiverTransform()); 194 | }); 195 | 196 | addEventListener('message', (event) => { 197 | const { action } = event.data; 198 | 199 | switch (action) { 200 | case 'rtctransform': 201 | setupPipe(event.data, createReceiverTransform()) 202 | break; 203 | default: 204 | break; 205 | } 206 | }); 207 | -------------------------------------------------------------------------------- /src/js/ovenplayer.js: -------------------------------------------------------------------------------- 1 | import {version} from 'version' 2 | import OvenPlayerSDK from './ovenplayer.sdk' 3 | import {checkAndGetContainerElement} from 'utils/validator' 4 | import View from './view/view'; 5 | 6 | function ovenPlayerFactory() { 7 | 8 | const OvenPlayer = {}; 9 | 10 | Object.assign(OvenPlayer, OvenPlayerSDK); 11 | 12 | OvenPlayer.create = function (container, options) { 13 | 14 | let containerElement = checkAndGetContainerElement(container); 15 | 16 | let player = View(containerElement); 17 | 18 | const playerInstance = OvenPlayerSDK.create(player.getMediaElementContainer(), options); 19 | 20 | player.setApi(playerInstance); 21 | 22 | OvenPlayerConsole.log("[OvenPlayer] v."+ version); 23 | 24 | return playerInstance; 25 | }; 26 | 27 | return OvenPlayer; 28 | } 29 | 30 | export default ovenPlayerFactory() 31 | -------------------------------------------------------------------------------- /src/js/ovenplayer.sdk.js: -------------------------------------------------------------------------------- 1 | import API from 'api/Api'; 2 | import {isWebRTC, checkAndGetContainerElement} from 'utils/validator'; 3 | import _ from "utils/underscore"; 4 | 5 | /** 6 | * Main OvenPlayerSDK object 7 | */ 8 | function ovenPlayerFactory() { 9 | 10 | const OvenPlayerSDK = {}; 11 | 12 | const playerList = OvenPlayerSDK.playerList = []; 13 | 14 | /** 15 | * Create player instance and return it. 16 | * 17 | * @param {string | dom element} container Id of container element or container element 18 | * @param {object} options The options 19 | */ 20 | OvenPlayerSDK.create = function (container, options) { 21 | 22 | if (!window.OvenPlayerConsole || Object.keys(window.OvenPlayerConsole).length === 0) { 23 | window.OvenPlayerConsole = {}; 24 | OvenPlayerConsole['log'] = function () { 25 | }; 26 | } 27 | 28 | let containerElement = checkAndGetContainerElement(container); 29 | 30 | const playerInstance = API(containerElement); 31 | playerInstance.init(options); 32 | 33 | playerList.push(playerInstance); 34 | 35 | return playerInstance; 36 | }; 37 | 38 | /** 39 | * Gets the player instance list. 40 | * 41 | * @return {array} The player list. 42 | */ 43 | OvenPlayerSDK.getPlayerList = function () { 44 | 45 | return playerList; 46 | }; 47 | 48 | /** 49 | * Gets the player instance by container id. 50 | * 51 | * @param {string} containerId The container identifier 52 | * @return {obeject | null} The player instance. 53 | */ 54 | OvenPlayerSDK.getPlayerByContainerId = function (containerId) { 55 | 56 | for (let i = 0; i < playerList.length; i++) { 57 | 58 | if (playerList[i].getContainerId() === containerId) { 59 | 60 | return playerList[i]; 61 | } 62 | } 63 | 64 | return null; 65 | }; 66 | 67 | /** 68 | * Gets the player instance by index. 69 | * 70 | * @param {number} index The index 71 | * @return {object | null} The player instance. 72 | */ 73 | OvenPlayerSDK.getPlayerByIndex = function (index) { 74 | 75 | const playerInstance = playerList[index]; 76 | 77 | if (playerInstance) { 78 | 79 | return playerInstance; 80 | } else { 81 | 82 | return null; 83 | } 84 | }; 85 | 86 | /** 87 | * Remove the player instance by playerInstance. 88 | * 89 | * @param {playerInstance} playerInstance 90 | * @return {null} 91 | */ 92 | OvenPlayerSDK.removePlayer = function (playerInstance) { 93 | 94 | for (let i = 0; i < playerList.length; i++) { 95 | 96 | if (playerList[i] === playerInstance) { 97 | playerList.splice(i, 1); 98 | } 99 | } 100 | }; 101 | 102 | /** 103 | * Generate webrtc source for player source type. 104 | * 105 | * @param {Object | Array} source webrtc source 106 | * @return {Array} Player source Object. 107 | */ 108 | OvenPlayerSDK.generateWebrtcUrls = function (sources) { 109 | return (_.isArray(sources) ? sources : [sources]).map(function (source, index) { 110 | if (source.host && isWebRTC(source.host) && source.application && source.stream) { 111 | return { 112 | file: source.host + "/" + source.application + "/" + source.stream, 113 | type: "webrtc", 114 | label: source.label ? source.label : "webrtc-" + (index + 1) 115 | }; 116 | } 117 | }); 118 | }; 119 | 120 | /** 121 | * Whether show the player core log or not. 122 | * 123 | * @param {boolean} boolean run debug mode or not. 124 | * @return {boolean} run debug mode or not. 125 | */ 126 | OvenPlayerSDK.debug = function (isDebugMode) { 127 | 128 | if (isDebugMode) { 129 | window.OvenPlayerConsole = {log: window['console']['log']}; 130 | } else { 131 | window.OvenPlayerConsole = { 132 | log: function () { 133 | } 134 | }; 135 | } 136 | return isDebugMode; 137 | }; 138 | 139 | return OvenPlayerSDK; 140 | } 141 | 142 | 143 | export default ovenPlayerFactory(); 144 | -------------------------------------------------------------------------------- /src/js/utils/captions/vttRegion.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 vtt.js Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | let VTTRegion = ""; 18 | 19 | var scrollSetting = { 20 | "": true, 21 | "up": true 22 | }; 23 | 24 | function findScrollSetting(value) { 25 | if (typeof value !== "string") { 26 | return false; 27 | } 28 | var scroll = scrollSetting[value.toLowerCase()]; 29 | return scroll ? value.toLowerCase() : false; 30 | } 31 | 32 | function isValidPercentValue(value) { 33 | return typeof value === "number" && (value >= 0 && value <= 100); 34 | } 35 | 36 | // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface 37 | VTTRegion = function() { 38 | var _width = 100; 39 | var _lines = 3; 40 | var _regionAnchorX = 0; 41 | var _regionAnchorY = 100; 42 | var _viewportAnchorX = 0; 43 | var _viewportAnchorY = 100; 44 | var _scroll = ""; 45 | 46 | Object.defineProperties(this, { 47 | "width": { 48 | enumerable: true, 49 | get: function() { 50 | return _width; 51 | }, 52 | set: function(value) { 53 | if (!isValidPercentValue(value)) { 54 | throw new Error("Width must be between 0 and 100."); 55 | } 56 | _width = value; 57 | } 58 | }, 59 | "lines": { 60 | enumerable: true, 61 | get: function() { 62 | return _lines; 63 | }, 64 | set: function(value) { 65 | if (typeof value !== "number") { 66 | throw new TypeError("Lines must be set to a number."); 67 | } 68 | _lines = value; 69 | } 70 | }, 71 | "regionAnchorY": { 72 | enumerable: true, 73 | get: function() { 74 | return _regionAnchorY; 75 | }, 76 | set: function(value) { 77 | if (!isValidPercentValue(value)) { 78 | throw new Error("RegionAnchorX must be between 0 and 100."); 79 | } 80 | _regionAnchorY = value; 81 | } 82 | }, 83 | "regionAnchorX": { 84 | enumerable: true, 85 | get: function() { 86 | return _regionAnchorX; 87 | }, 88 | set: function(value) { 89 | if(!isValidPercentValue(value)) { 90 | throw new Error("RegionAnchorY must be between 0 and 100."); 91 | } 92 | _regionAnchorX = value; 93 | } 94 | }, 95 | "viewportAnchorY": { 96 | enumerable: true, 97 | get: function() { 98 | return _viewportAnchorY; 99 | }, 100 | set: function(value) { 101 | if (!isValidPercentValue(value)) { 102 | throw new Error("ViewportAnchorY must be between 0 and 100."); 103 | } 104 | _viewportAnchorY = value; 105 | } 106 | }, 107 | "viewportAnchorX": { 108 | enumerable: true, 109 | get: function() { 110 | return _viewportAnchorX; 111 | }, 112 | set: function(value) { 113 | if (!isValidPercentValue(value)) { 114 | throw new Error("ViewportAnchorX must be between 0 and 100."); 115 | } 116 | _viewportAnchorX = value; 117 | } 118 | }, 119 | "scroll": { 120 | enumerable: true, 121 | get: function() { 122 | return _scroll; 123 | }, 124 | set: function(value) { 125 | var setting = findScrollSetting(value); 126 | // Have to check for false as an empty string is a legal value. 127 | if (setting === false) { 128 | throw new SyntaxError("An invalid or illegal string was specified."); 129 | } 130 | _scroll = setting; 131 | } 132 | } 133 | }); 134 | } 135 | 136 | export default VTTRegion; -------------------------------------------------------------------------------- /src/js/utils/date.js: -------------------------------------------------------------------------------- 1 | export const now = Date.now || function() { 2 | return new Date().getTime(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/js/utils/deepMerge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a deep merge of the `source` object's properties into the `target` object. 3 | * For nested objects, the function recurses; otherwise it directly overwrites values. 4 | * 5 | * @param {Object} target - The object to which properties are merged. 6 | * @param {Object} source - The object containing properties to be copied. 7 | * @returns {Object} The modified `target` object reference. 8 | * 9 | * @example 10 | * const objA = { 11 | * a: 1, 12 | * b: { x: 10, y: 20 } 13 | * }; 14 | * const objB = { 15 | * b: { y: 999, z: 50 }, 16 | * c: 3 17 | * }; 18 | * 19 | * deepMerge(objA, objB); 20 | * Result: objA = { 21 | * a: 1, 22 | * b: { x: 10, y: 999, z: 50 }, 23 | * c: 3 24 | * } 25 | */ 26 | export default function deepMerge(target, source) { 27 | for (let key in source) { 28 | if (Object.prototype.hasOwnProperty.call(source, key)) { 29 | if ( 30 | typeof source[key] === "object" && 31 | source[key] !== null && 32 | typeof target[key] === "object" && 33 | target[key] !== null 34 | ) { 35 | deepMerge(target[key], source[key]); 36 | } else { 37 | target[key] = source[key]; 38 | } 39 | } 40 | } 41 | return target; 42 | } 43 | -------------------------------------------------------------------------------- /src/js/utils/eme.js: -------------------------------------------------------------------------------- 1 | export function arrayToString(array) { 2 | var uint16array = new Uint16Array(array.buffer); 3 | return String.fromCharCode.apply(null, uint16array); 4 | } 5 | 6 | export function base64DecodeUint8Array(input) { 7 | 8 | var raw = window.atob(input); 9 | var rawLength = raw.length; 10 | var array = new Uint8Array(new ArrayBuffer(rawLength)); 11 | 12 | for (let i = 0; i < rawLength; i++) 13 | array[i] = raw.charCodeAt(i); 14 | return array; 15 | } 16 | 17 | export function base64EncodeUint8Array(input) { 18 | var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 19 | var output = ""; 20 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 21 | var i = 0; 22 | 23 | while (i < input.length) { 24 | chr1 = input[i++]; 25 | chr2 = i < input.length ? input[i++] : Number.NaN; 26 | chr3 = i < input.length ? input[i++] : Number.NaN; 27 | 28 | enc1 = chr1 >> 2; 29 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 30 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 31 | enc4 = chr3 & 63; 32 | 33 | if (isNaN(chr2)) { 34 | enc3 = enc4 = 64; 35 | } else if (isNaN(chr3)) { 36 | enc4 = 64; 37 | } 38 | output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + 39 | keyStr.charAt(enc3) + keyStr.charAt(enc4); 40 | } 41 | return output; 42 | } 43 | 44 | export function stringToUint16Array(string) { 45 | // 2 bytes for each char 46 | var buffer = new ArrayBuffer(string.length * 2); 47 | var array = new Uint16Array(buffer); 48 | 49 | for (var i = 0; i < string.length; i++) { 50 | array[i] = string.charCodeAt(i); 51 | } 52 | 53 | return array; 54 | }; 55 | 56 | 57 | export function concatInitDataIdAndCertificate({ 58 | id, 59 | initData, 60 | cert 61 | }) { 62 | 63 | try { 64 | if (typeof id === 'string') { 65 | id = stringToUint16Array(id); 66 | } // layout: 67 | // [initData] 68 | // [4 byte: idLength] 69 | // [idLength byte: id] 70 | // [4 byte:certLength] 71 | // [certLength byte: cert] 72 | 73 | var offset = 0; 74 | var buffer = new ArrayBuffer(initData.byteLength + 4 + id.byteLength + 4 + cert.byteLength); 75 | var dataView = new DataView(buffer); 76 | var initDataArray = new Uint8Array(buffer, offset, initData.byteLength); 77 | initDataArray.set(initData); 78 | offset += initData.byteLength; 79 | dataView.setUint32(offset, id.byteLength, true); 80 | offset += 4; 81 | var idArray = new Uint16Array(buffer, offset, id.length); 82 | idArray.set(id); 83 | offset += idArray.byteLength; 84 | dataView.setUint32(offset, cert.byteLength, true); 85 | offset += 4; 86 | var certArray = new Uint8Array(buffer, offset, cert.byteLength); 87 | certArray.set(cert); 88 | var data = new Uint8Array(buffer, 0, buffer.byteLength); 89 | return data; 90 | } catch (error) { 91 | return null; 92 | } 93 | }; -------------------------------------------------------------------------------- /src/js/utils/getTouchSection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Detect if a touch event happened on the left, middle, or right side of the element 3 | * @param {MouseEvent} event 4 | * @returns {'left'|'middle'|'right'} 5 | */ 6 | export default function getTouchSection(event) { 7 | /** 8 | * @type {DOMRect} 9 | */ 10 | const clientRect = event.target.getBoundingClientRect(); 11 | 12 | // is it on the right 13 | if (event.offsetX >= ((2/3)*clientRect.width)) { 14 | return 'right'; 15 | } 16 | 17 | // is it on the middle 18 | if (event.offsetX >= ((1/3)*clientRect.width)) { 19 | return 'middle'; 20 | } 21 | 22 | return 'left'; 23 | } -------------------------------------------------------------------------------- /src/js/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 5. 24.. 3 | */ 4 | 5 | const logger = function(id){ 6 | const that = {}; 7 | let prevConsoleLog = null; 8 | 9 | window.OvenPlayerConsole = {log : window['console']['log']}; 10 | 11 | that.enable = () =>{ 12 | if(prevConsoleLog == null){ 13 | return; 14 | } 15 | OvenPlayerConsole['log'] = prevConsoleLog; 16 | }; 17 | that.disable = () =>{ 18 | prevConsoleLog = console.log; 19 | OvenPlayerConsole['log'] = function(){}; 20 | }; 21 | /*that.log = () => { 22 | 23 | };*/ 24 | that.destroy = () =>{ 25 | window.OvenPlayerConsole = null; 26 | }; 27 | 28 | return that; 29 | }; 30 | 31 | 32 | export default logger; -------------------------------------------------------------------------------- /src/js/utils/sizeHumanizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 11. 14.. 3 | */ 4 | 5 | const sizeHumanizer = function (bytes, si, postpix) { 6 | let thresh = si ? 1000 : 1024; 7 | if(Math.abs(bytes) < thresh) { 8 | return bytes + ' B'; 9 | } 10 | let unit = postpix||"B"; 11 | let units = ['k'+unit,'M'+unit,'G'+unit,'T'+unit,'P'+unit,'E'+unit,'Z'+unit,'Y'+unit]; 12 | // ? ['kB','MB','GB','TB','PB','EB','ZB','YB']: ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']; 13 | let u = -1; 14 | do { 15 | bytes /= thresh; 16 | ++u; 17 | } while(Math.abs(bytes) >= thresh && u < units.length - 1); 18 | return bytes.toFixed(1)+units[u]; 19 | } 20 | 21 | export default sizeHumanizer; -------------------------------------------------------------------------------- /src/js/utils/strings.js: -------------------------------------------------------------------------------- 1 | import _ from 'utils/underscore'; 2 | 3 | export function trim(string) { 4 | return string ? string.replace(/^\s+|\s+$/g, '') : ""; 5 | } 6 | 7 | /** 8 | * extractExtension 9 | * 10 | * @param {string} path for url 11 | * @return {string} Extension 12 | */ 13 | export const extractExtension = function(path) { 14 | if(!path || path.substr(0,4)=='rtmp') { 15 | return ""; 16 | } 17 | function getAzureFileFormat(path) { 18 | let extension = ""; 19 | if ((/[(,]format=mpd-/i).test(path)) { 20 | extension = 'mpd'; 21 | }else if ((/[(,]format=m3u8-/i).test(path)) { 22 | extension = 'm3u8'; 23 | } 24 | return extension; 25 | } 26 | 27 | let azuredFormat = getAzureFileFormat(path); 28 | if(azuredFormat) { 29 | return azuredFormat; 30 | } 31 | path = path.split('?')[0].split('#')[0]; 32 | if(path.lastIndexOf('.') > -1) { 33 | return path.substr(path.lastIndexOf('.') + 1, path.length).toLowerCase(); 34 | }else{ 35 | return ""; 36 | } 37 | }; 38 | 39 | 40 | /** 41 | * naturalHms 42 | * 43 | * @param {number | string} second The second 44 | * @return {string} formatted String 45 | */ 46 | export function naturalHms(second) { 47 | let secNum = parseInt(second, 10); 48 | if(!second){ 49 | return "00:00"; 50 | } 51 | let hours = Math.floor(secNum / 3600); 52 | let minutes = Math.floor((secNum - (hours * 3600)) / 60); 53 | let seconds = secNum - (hours * 3600) - (minutes * 60); 54 | 55 | //if (hours > 0) {minutes = "0"+minutes;} 56 | if (minutes < 10) {minutes = "0"+minutes;} 57 | if (seconds < 10) {seconds = "0"+seconds;} 58 | 59 | if (hours > 0) { 60 | return hours+':'+minutes+':'+seconds; 61 | } else { 62 | return minutes+':'+seconds; 63 | } 64 | } 65 | 66 | 67 | export function hmsToSecond(str, frameRate) { 68 | if(!str) { 69 | return 0; 70 | } 71 | if(_.isNumber(str) && !_.isNaN(str)){ 72 | return str; 73 | } 74 | str = str.replace(',', '.'); 75 | let arr = str.split(':'); 76 | let arrLength = arr.length; 77 | let sec = 0; 78 | if (str.slice(-1) === 's'){ 79 | sec = parseFloat(str); 80 | }else if (str.slice(-1) === 'm'){ 81 | sec = parseFloat(str) * 60; 82 | }else if (str.slice(-1) === 'h'){ 83 | sec = parseFloat(str) * 3600; 84 | }else if (arrLength > 1) { 85 | var secIndex = arrLength - 1; 86 | if (arrLength === 4) { 87 | if (frameRate) { 88 | sec = parseFloat(arr[secIndex]) / frameRate; 89 | } 90 | secIndex -= 1; 91 | } 92 | sec += parseFloat(arr[secIndex]); 93 | sec += parseFloat(arr[secIndex - 1]) * 60; 94 | if (arrLength >= 3) { 95 | sec += parseFloat(arr[secIndex - 2]) * 3600; 96 | } 97 | } else { 98 | sec = parseFloat(str); 99 | } 100 | if (_.isNaN(sec)) { 101 | return 0; 102 | } 103 | return sec; 104 | } -------------------------------------------------------------------------------- /src/js/utils/validator.js: -------------------------------------------------------------------------------- 1 | import {extractExtension} from "utils/strings"; 2 | 3 | export const isRtmp = function (file, type) { 4 | if (file) { 5 | return (file.indexOf('rtmp:') == 0 || type == 'rtmp'); 6 | } 7 | }; 8 | export const isWebRTC = function (file, type) { 9 | if (file) { 10 | return (file.indexOf('ws:') === 0 || file.indexOf('wss:') === 0 || type === 'webrtc'); 11 | } 12 | return false; 13 | }; 14 | export const isHls = function (file, type) { 15 | if (file) { 16 | return (type === 'hls' || type === 'm3u8' || type === 'application/vnd.apple.mpegurl' || extractExtension(file) == 'm3u8'); 17 | 18 | } 19 | }; 20 | export const isDash = function (file, type) { 21 | if (file) { 22 | return (type === 'mpd' || type === 'dash' || type === 'application/dash+xml' || extractExtension(file) == 'mpd'); 23 | 24 | } 25 | }; 26 | 27 | export const checkAndGetContainerElement = function (container) { 28 | if (!container) { 29 | 30 | return null; 31 | } 32 | 33 | let containerElement = null; 34 | 35 | if (typeof container === 'string') { 36 | 37 | containerElement = document.getElementById(container); 38 | } else if (container.nodeType) { 39 | 40 | containerElement = container; 41 | } else { 42 | 43 | return null; 44 | } 45 | 46 | return containerElement; 47 | }; -------------------------------------------------------------------------------- /src/js/utils/webpack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utils for webpack 3 | */ 4 | 5 | export const getScriptPath = function(scriptName) { 6 | const scripts = document.getElementsByTagName('script'); 7 | for (let i = 0; i < scripts.length; i++) { 8 | const src = scripts[i].src; 9 | if (src) { 10 | const index = src.lastIndexOf('/' + scriptName); 11 | if (index >= 0) { 12 | return src.substr(0, index + 1); 13 | } 14 | } 15 | } 16 | return ''; 17 | }; 18 | -------------------------------------------------------------------------------- /src/js/version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 6. 29.. 3 | */ 4 | export const version = __VERSION__; 5 | -------------------------------------------------------------------------------- /src/js/view/components/controls/frameButtons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from "view/engine/OvenTemplate"; 5 | import LA$ from 'utils/likeA$'; 6 | import { 7 | ERROR, 8 | STATE_IDLE, 9 | STATE_PLAYING, 10 | STATE_STALLED, 11 | STATE_LOADING, 12 | STATE_COMPLETE, 13 | STATE_PAUSED, 14 | STATE_ERROR, 15 | PLAYER_STATE 16 | } from "api/constants"; 17 | 18 | const FrameButtons = function ($container, api) { 19 | const onRendered = function($current, template){ 20 | }; 21 | const onDestroyed = function(template){ 22 | 23 | }; 24 | const events = { 25 | "click .op-frame-button" : function(event, $current, template){ 26 | event.preventDefault(); 27 | let value = LA$(event.currentTarget).attr("op-data-value"); 28 | if(value){ 29 | api.seekFrame(parseInt(value)); 30 | } 31 | } 32 | }; 33 | 34 | return OvenTemplate($container, "FrameButtons", api.getConfig(), null, events, onRendered, onDestroyed ); 35 | }; 36 | 37 | export default FrameButtons; 38 | -------------------------------------------------------------------------------- /src/js/view/components/controls/frameButtonsTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return ( 3 | '
'+ 4 | ''+ 5 | ''+ 6 | ''+ 7 | ''+ 8 | '
' 9 | ); 10 | }; -------------------------------------------------------------------------------- /src/js/view/components/controls/fullScreenButtonTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return ( 3 | `` 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /src/js/view/components/controls/mainTemplate.js: -------------------------------------------------------------------------------- 1 | 2 | const Controls = function(uiText, hasPlaylist){ 3 | return ( 4 | `
`+ 5 | `
` + 6 | `
` + 7 | `
` + 8 | `
` + 9 | `
`+ 10 | `
${hasPlaylist?``:``}
`+ 11 | `
`+ 12 | `
`+ 13 | `
` + 14 | `
` + 15 | `
` + 16 | `
` + 17 | `
`); 18 | 19 | }; 20 | 21 | 22 | export default Controls; 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/view/components/controls/playButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from "view/engine/OvenTemplate"; 5 | import { 6 | ERROR, 7 | STATE_IDLE, 8 | STATE_PLAYING, 9 | STATE_STALLED, 10 | STATE_LOADING, 11 | STATE_COMPLETE, 12 | STATE_PAUSED, 13 | STATE_ERROR, 14 | PLAYER_STATE, 15 | STATE_AD_LOADED, 16 | STATE_AD_PLAYING, 17 | STATE_AD_PAUSED, 18 | STATE_AD_COMPLETE, 19 | } from "api/constants"; 20 | 21 | const PlayButton = function ($container, api) { 22 | let $iconPlay = "", 23 | $iconPause = "", 24 | $iconReplay = "", 25 | $buttonBack = "", 26 | $buttonForward = "", 27 | $textBack = "", 28 | $textForward = ""; 29 | 30 | 31 | 32 | function setButtonState(state){ 33 | $iconPlay.hide(); 34 | $iconPause.hide(); 35 | $iconReplay.hide(); 36 | if(state === STATE_PLAYING || state === STATE_AD_PLAYING || state === STATE_LOADING || state === STATE_STALLED){ 37 | $iconPause.show(); 38 | }else if(state === STATE_PAUSED || state === STATE_AD_PAUSED){ 39 | $iconPlay.show(); 40 | }else if(state === STATE_COMPLETE){ 41 | $iconReplay.show(); 42 | }else{ 43 | $iconPlay.show(); 44 | } 45 | }; 46 | 47 | 48 | 49 | const onRendered = function($current, template){ 50 | $iconPlay = $current.find(".op-play-button .op-play"); 51 | $iconPause = $current.find(".op-play-button .op-pause"); 52 | $iconReplay = $current.find(".op-play-button .op-replay"); 53 | $buttonBack = $current.find('.op-seek-button-back'); 54 | $buttonForward = $current.find('.op-seek-button-forward'); 55 | $textBack = $current.find('.op-seek-back-text'); 56 | $textForward = $current.find('.op-seek-forward-text'); 57 | 58 | api.on(PLAYER_STATE, function(data){ 59 | if(data && data.newstate){ 60 | setButtonState(data.newstate); 61 | } 62 | }, template); 63 | 64 | if (!api.getConfig().showSeekControl) { 65 | $buttonBack.hide(); 66 | $buttonForward.hide(); 67 | } 68 | 69 | let seekInterval = api.getConfig().seekControlInterval; 70 | 71 | if (seekInterval) { 72 | 73 | $textBack.text(seekInterval); 74 | $textForward.text(seekInterval); 75 | } else { 76 | 77 | $textBack.text(10); 78 | $textForward.text(10); 79 | } 80 | }; 81 | const onDestroyed = function(template){ 82 | api.off(PLAYER_STATE, null, template); 83 | }; 84 | const events = { 85 | "click .op-play-button" : function(event, $current, template){ 86 | event.preventDefault(); 87 | let currentState = api.getState(); 88 | let playlist = api.getPlaylist(); 89 | let currentPlaylistIndex = api.getCurrentPlaylist(); 90 | 91 | if (currentState === STATE_IDLE) { 92 | api.play(); 93 | } else if (currentState === STATE_PLAYING || currentState === STATE_AD_PLAYING) { 94 | api.pause(); 95 | } else if (currentState === STATE_LOADING || currentState === STATE_STALLED) { 96 | api.stop(); 97 | } else if (currentState === STATE_PAUSED || currentState === STATE_AD_PAUSED) { 98 | api.play(); 99 | } else if (currentState === STATE_ERROR) { 100 | api.setCurrentSource(api.getCurrentSource()); 101 | } else if (currentState === STATE_COMPLETE) { 102 | if(playlist.length === (currentPlaylistIndex+1)){ 103 | api.seek(0); 104 | api.play(); 105 | } 106 | } 107 | }, 108 | "click .op-seek-button-back" : function(event, $current, template) { 109 | 110 | let seekInterval = api.getConfig().seekControlInterval; 111 | 112 | if (!seekInterval) { 113 | seekInterval = 10; 114 | } 115 | 116 | let time = api.getPosition() - seekInterval; 117 | 118 | if (time < 0) { 119 | time = 0; 120 | } 121 | 122 | api.seek(time); 123 | }, 124 | "click .op-seek-button-forward" : function(event, $current, template) { 125 | 126 | let seekInterval = api.getConfig().seekControlInterval; 127 | 128 | if (!seekInterval) { 129 | seekInterval = 10; 130 | } 131 | 132 | let time = api.getPosition() + seekInterval; 133 | 134 | if (time > api.getDuration()) { 135 | time = api.getDuration(); 136 | } 137 | 138 | api.seek(time); 139 | } 140 | }; 141 | 142 | return OvenTemplate($container, "PlayButton", api.getConfig(), null, events, onRendered, onDestroyed ); 143 | }; 144 | 145 | export default PlayButton; 146 | -------------------------------------------------------------------------------- /src/js/view/components/controls/playButtonTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return ( 3 | `
` + 4 | `` + 9 | `` + 13 | `` + 17 | `
` 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/view/components/controls/playlistPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 06/03/2019. 3 | */ 4 | import OvenTemplate from "view/engine/OvenTemplate"; 5 | import {naturalHms} from "utils/strings" 6 | import {playlistItemTemplate} from "view/components/controls/playlistPanelTemplate"; 7 | import LA$ from "utils/likeA$"; 8 | import { 9 | PLAYER_RESIZED, 10 | PLAYLIST_CHANGED 11 | } from "api/constants"; 12 | 13 | const PlaylistPanel = function($container, api){ 14 | const $root = LA$(api.getContainerElement()); 15 | 16 | let $playlistPanel = ""; 17 | let playlist = api.getPlaylist(); 18 | let totalCount = playlist.length; 19 | 20 | let pageSize = 6; 21 | let page = 0; 22 | let pagedList = []; 23 | 24 | if($root.width() > 576){ 25 | pageSize = 6; 26 | }else if($root.width() <= 576) { 27 | pageSize = 1; 28 | } 29 | 30 | function pagenate(page){ 31 | let totalPageCount = Math.ceil(totalCount / pageSize); 32 | let currentPlaylistIndex = api.getCurrentPlaylist(); 33 | 34 | pagedList = playlist.slice(page*pageSize, (page*pageSize)+pageSize); 35 | 36 | $playlistPanel.find(".op-playlist-body-row").removeChild(); 37 | $playlistPanel.find(".op-arrow-left").removeClass("disable"); 38 | $playlistPanel.find(".op-arrow-right").removeClass("disable"); 39 | 40 | for(let i = 0; i < pagedList.length; i ++){ 41 | let originalItemIndex = (page * pageSize) + i; 42 | pagedList[i].index = originalItemIndex; 43 | $playlistPanel.find(".op-playlist-body-row").get().append( 44 | createAndSelectElement(playlistItemTemplate(pagedList[i], currentPlaylistIndex === originalItemIndex)) 45 | ); 46 | } 47 | 48 | if(page === 0){ 49 | $playlistPanel.find(".op-arrow-left").addClass("disable"); 50 | } 51 | if(page+1 === totalPageCount){ 52 | $playlistPanel.find(".op-arrow-right").addClass("disable"); 53 | } 54 | }; 55 | function findCurrentPage(){ 56 | let currentPlaylistIndex = api.getCurrentPlaylist(); 57 | return Math.ceil((currentPlaylistIndex+1)/ pageSize) -1 58 | }; 59 | function createAndSelectElement(html) { 60 | const newElement = document.createElement('div'); 61 | newElement.innerHTML = html; 62 | return newElement.firstChild; 63 | } 64 | const onRendered = function($current, template){ 65 | $playlistPanel = $current; 66 | 67 | page = findCurrentPage(); 68 | pagenate(page); 69 | 70 | api.on(PLAYER_RESIZED, function(size){ 71 | if( (size === "xsmall") && pageSize === 6 ){ 72 | pageSize = 1; 73 | page = findCurrentPage(); 74 | pagenate(page); 75 | }else if((size === "small" || size === "medium" || size === "large") && pageSize === 1){ 76 | pageSize = 6; 77 | page = findCurrentPage(); 78 | pagenate(page); 79 | } 80 | },template); 81 | 82 | api.on(PLAYLIST_CHANGED, function(size){ 83 | page = findCurrentPage(); 84 | pagenate(page); 85 | },template); 86 | 87 | 88 | //돔에서 엘리먼트가 제거되면 이벤트도 같이 제거 되어 버리기 때문에 일단 이렇게 해당 템플릿내에서만 live 되도록 처리. 추후 TemplateEngine에 개선 89 | $current.get().addEventListener("click",function(evt){ 90 | var gtarget = evt.target; 91 | while (gtarget){ 92 | if (LA$(gtarget).hasClass("op-playlist-card")){ 93 | api.setCurrentPlaylist(parseInt(LA$(gtarget).attr("data-index"))); 94 | return; 95 | } 96 | gtarget = gtarget.parentElement; 97 | } 98 | }, true); 99 | 100 | }; 101 | const onDestroyed = function(template){ 102 | api.off(PLAYER_RESIZED, null, template); 103 | api.off(PLAYLIST_CHANGED, null, template); 104 | }; 105 | const events = { 106 | "click .btn-close" : function(event, $current, template){ 107 | event.preventDefault(); 108 | template.destroy(); 109 | 110 | }, 111 | "click .op-arrow-left" : function(event, $current, template){ 112 | event.preventDefault(); 113 | if( !LA$(event.target).hasClass("disable") ){ 114 | page--; 115 | pagenate(page); 116 | } 117 | }, 118 | "click .op-arrow-right" : function(event, $current, template){ 119 | event.preventDefault(); 120 | if( !LA$(event.target).hasClass("disable") ){ 121 | page++; 122 | pagenate(page); 123 | } 124 | }/*, 125 | "click .op-playlist-card" : function(event, $current, template){ 126 | event.preventDefault(); 127 | }*/ 128 | }; 129 | 130 | return OvenTemplate($container, "PlaylistPanel", api.getConfig(), playlist, events, onRendered, onDestroyed ); 131 | }; 132 | 133 | export default PlaylistPanel; 134 | -------------------------------------------------------------------------------- /src/js/view/components/controls/playlistPanelTemplate.js: -------------------------------------------------------------------------------- 1 | import {naturalHms} from "utils/strings" 2 | 3 | export default (uiText, playlist) => { 4 | return (`
` + 5 | `
${uiText.playlist}
` + 6 | `
` + 7 | `
` + 8 | `
`+ 9 | `
`+ 10 | `
` + 11 | `` + 12 | `
`+ 13 | `
`+ 14 | `
`); 15 | }; 16 | 17 | export const playlistItemTemplate = (data, isActive) => { 18 | return ( 19 | `
` + 20 | `
${data.image ? ``:``} ${data.duration ? `${naturalHms(data.duration)}`: ``}
`+ 21 | `
${data.title}
`+ 22 | `
` 23 | ); 24 | }; -------------------------------------------------------------------------------- /src/js/view/components/controls/progressBarTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return ( 3 | `
` + 4 | `
` + 5 | `
` + 6 | `
` + 7 | `
` + 8 | `
` + 9 | `
` + 10 | `
` + 11 | `
` + 12 | `
` + 13 | `` + 14 | `00:00` + 15 | `
` 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/js/view/components/controls/settingButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2019. 5. 17.. 3 | */ 4 | import OvenTemplate from "view/engine/OvenTemplate"; 5 | import Panels from "view/components/controls/settingPanel/main"; 6 | import PanelManager from "view/global/PanelManager"; 7 | import { 8 | PROVIDER_RTMP 9 | } from "api/constants"; 10 | 11 | let PANEL_TITLE = { 12 | "speed": "Speed", 13 | "speedUnit": "x", 14 | "source": "Source", 15 | "quality": "Quality", 16 | "audioTrack": "Audio", 17 | "caption": "Caption", 18 | "display": "Display", 19 | "zoom": "Zoom" 20 | }; 21 | const SettingButton = function ($container, api) { 22 | let panelManager = PanelManager(); 23 | 24 | function generateMainData(api) { 25 | let panel = { 26 | id: "panel-" + new Date().getTime(), 27 | title: "Settings", 28 | body: [], 29 | isRoot: true, 30 | panelType: "" 31 | }; 32 | 33 | let playerConfig = api.getConfig(); 34 | 35 | if (playerConfig && playerConfig.systemText) { 36 | Object.keys(PANEL_TITLE).forEach(title => { 37 | PANEL_TITLE[title] = playerConfig.systemText.ui.setting[title]; 38 | }); 39 | panel.title = playerConfig.systemText.ui.setting.title; 40 | } 41 | let sources = api.getSources(); 42 | let currentSource = sources && sources.length > 0 ? sources[api.getCurrentSource()] : null; 43 | 44 | let qualityLevels = api.getQualityLevels(); 45 | let currentQuality = qualityLevels && qualityLevels.length > 0 ? qualityLevels[api.getCurrentQuality()] : null; 46 | 47 | let audioTracks = api.getAudioTracks(); 48 | let currentAudioTrack = audioTracks && audioTracks.length > 0 ? audioTracks[api.getCurrentAudioTrack()] : null; 49 | 50 | let captions = api.getCaptionList(); 51 | let currentCaption = api.getCurrentCaption(); 52 | 53 | let framerate = api.getFramerate(); 54 | 55 | let allowZoom = playerConfig.showZoomSettings; 56 | 57 | if (currentSource) { 58 | let body = { 59 | title: PANEL_TITLE.speed, 60 | value: api.getPlaybackRate() + PANEL_TITLE.speedUnit, 61 | description: api.getPlaybackRate() + PANEL_TITLE.speedUnit, 62 | panelType: "speed", 63 | hasNext: true 64 | }; 65 | panel.body.push(body); 66 | } 67 | 68 | if (sources && sources.length > 1) { 69 | 70 | let body = { 71 | title: PANEL_TITLE.source, 72 | value: currentSource ? currentSource.label : "Default", 73 | description: currentSource ? currentSource.label : "Default", 74 | panelType: "source", 75 | hasNext: true 76 | }; 77 | 78 | panel.body.push(body); 79 | } 80 | if (qualityLevels && qualityLevels.length > 0) { 81 | 82 | let body = { 83 | title: PANEL_TITLE.quality, 84 | value: currentQuality ? currentQuality.label : "Default", 85 | description: currentQuality ? currentQuality.label : "Default", 86 | panelType: "quality", 87 | hasNext: true 88 | }; 89 | 90 | panel.body.push(body); 91 | } 92 | 93 | if (audioTracks && audioTracks.length > 0) { 94 | 95 | let body = { 96 | title: PANEL_TITLE.audioTrack, 97 | value: currentAudioTrack ? currentAudioTrack.label : "Default", 98 | description: currentAudioTrack ? currentAudioTrack.label : "Default", 99 | panelType: "audioTrack", 100 | hasNext: true 101 | }; 102 | 103 | panel.body.push(body); 104 | } 105 | 106 | if (captions && captions.length > 0) { 107 | 108 | let body = { 109 | title: PANEL_TITLE.caption, 110 | value: captions[currentCaption] ? captions[currentCaption].label : "OFF", 111 | description: captions[currentCaption] ? captions[currentCaption].label : "OFF", 112 | panelType: "caption", 113 | hasNext: true 114 | }; 115 | 116 | panel.body.push(body); 117 | } 118 | if (framerate > 0) { 119 | let body = { 120 | title: PANEL_TITLE.display, 121 | value: api.isTimecodeMode() ? "Play time" : "Framecode", 122 | description: api.isTimecodeMode() ? "Play time" : "Framecode", 123 | panelType: "display", 124 | hasNext: true 125 | }; 126 | 127 | panel.body.push(body); 128 | } 129 | 130 | if (allowZoom) { 131 | let body = { 132 | title: PANEL_TITLE.zoom, 133 | value: Math.round(api.getZoomFactor() * 100) + "%", 134 | description: Math.round(api.getZoomFactor() * 100) + "%", 135 | panelType: "zoom", 136 | hasNext: true 137 | }; 138 | panel.body.push(body); 139 | } 140 | 141 | return panel; 142 | }; 143 | 144 | const onRendered = function ($current, template) { 145 | }; 146 | const onDestroyed = function (template) { 147 | }; 148 | const events = { 149 | "click .op-setting-button": function (event, $current, template) { 150 | event.preventDefault(); 151 | let $parent = $current.closest(".op-controls-container"); 152 | if (panelManager.size() > 0) { 153 | panelManager.clear(); 154 | } else { 155 | let panelData = generateMainData(api); 156 | panelManager.add(Panels($parent, api, panelData)); 157 | } 158 | }, 159 | }; 160 | 161 | return OvenTemplate($container, "SettingButton", api.getConfig(), null, events, onRendered, onDestroyed); 162 | }; 163 | 164 | 165 | export default SettingButton; 166 | -------------------------------------------------------------------------------- /src/js/view/components/controls/settingButtonTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return (``); 3 | }; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/audioTrackPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | import { 8 | AUDIO_TRACK_CHANGED 9 | } from "api/constants"; 10 | import _ from "utils/underscore"; 11 | 12 | const AudioTrackPanel = function ($container, api, data) { 13 | const $root = LA$(api.getContainerElement()); 14 | let panelManager = PanelManager(); 15 | 16 | data.setFront = function (isFront) { 17 | if (isFront) { 18 | $root.find("#" + data.id).removeClass("background"); 19 | } else { 20 | $root.find("#" + data.id).addClass("background"); 21 | } 22 | }; 23 | const onRendered = function ($current, template) { 24 | api.on(AUDIO_TRACK_CHANGED, function (data) { 25 | _.forEach($root.find("#" + template.data.id).find(".op-setting-item").get(), function (panel) { 26 | let $panel = LA$(panel); 27 | 28 | if ($panel.find(".op-setting-item-checked").hasClass("op-show")) { 29 | $panel.find(".op-setting-item-checked").removeClass("op-show"); 30 | } 31 | if (data.currentAudioTrack === parseInt($panel.attr("op-data-value"))) { 32 | $panel.find(".op-setting-item-checked").addClass("op-show"); 33 | } 34 | }); 35 | }, template); 36 | }; 37 | const onDestroyed = function (template) { 38 | api.off(AUDIO_TRACK_CHANGED, null, template); 39 | }; 40 | const events = { 41 | "click .op-setting-item": function (event, $current, template) { 42 | event.preventDefault(); 43 | let value = LA$(event.currentTarget).attr("op-data-value"); 44 | api.setCurrentAudioTrack(parseInt(value)); 45 | panelManager.clear(); 46 | }, 47 | "click .op-setting-title": function (event, $current, template) { 48 | event.preventDefault(); 49 | panelManager.removeLastItem(); 50 | } 51 | }; 52 | 53 | return OvenTemplate($container, "AudioTrackPanel", api.getConfig(), data, events, onRendered, onDestroyed); 54 | 55 | }; 56 | 57 | export default AudioTrackPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/captionPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | 8 | 9 | const CaptionPanel = function($container, api, data){ 10 | const $root = LA$(api.getContainerElement()); 11 | let panelManager = PanelManager(); 12 | 13 | data.setFront = function(isFront){ 14 | if(isFront){ 15 | $root.find("#"+data.id).removeClass("background"); 16 | }else{ 17 | $root.find("#"+data.id).addClass("background"); 18 | } 19 | }; 20 | const onRendered = function($current, template){ 21 | //Do nothing 22 | }; 23 | const onDestroyed = function(template){ 24 | //Do nothing 25 | }; 26 | const events = { 27 | "click .op-setting-item": function (event, $current, template) { 28 | event.preventDefault(); 29 | let value = LA$(event.currentTarget).attr("op-data-value"); 30 | api.setCurrentCaption(parseFloat(value)); 31 | panelManager.clear(); 32 | }, 33 | "click .op-setting-title" : function(event, $current, template){ 34 | event.preventDefault(); 35 | panelManager.removeLastItem(); 36 | } 37 | }; 38 | 39 | return OvenTemplate($container, "CaptionPanel", api.getConfig(), data, events, onRendered, onDestroyed ); 40 | 41 | }; 42 | 43 | export default CaptionPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/mainTemplate.js: -------------------------------------------------------------------------------- 1 | import _ from "utils/underscore"; 2 | 3 | export default (uiText, data) => { 4 | let elements = '
' + 5 | '
' + 6 | '
' + 7 | (data.isRoot ? '' : '<') + 8 | ''+data.title+'' + 9 | '
'+ 10 | '
' + 11 | '
'; 12 | _.forEach(data.body, function(body){ 13 | elements += settingItemTemplate(body, data.useCheck); 14 | }); 15 | elements+= '
' + 16 | '
'; 17 | return elements; 18 | }; 19 | 20 | 21 | export const settingItemTemplate = (data, useCheck) => { 22 | return ( 23 | '
' + 24 | (useCheck?'':'' )+ 25 | ''+data.title+'' + 26 | (data.hasNext?'>'+data.description+'' : '' )+ 27 | '
' 28 | ); 29 | }; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/qualityPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | import _ from "utils/underscore"; 8 | import { 9 | CONTENT_LEVEL_CHANGED 10 | } from "api/constants"; 11 | 12 | const QualityPanel = function($container, api, data){ 13 | const $root = LA$(api.getContainerElement()); 14 | let panelManager = PanelManager(); 15 | 16 | data.setFront = function(isFront){ 17 | if(isFront){ 18 | $root.find("#"+data.id).removeClass("background"); 19 | }else{ 20 | $root.find("#"+data.id).addClass("background"); 21 | } 22 | }; 23 | const onRendered = function($current, template){ 24 | 25 | //This assistants UI when quality level changes. When you open setting panels. 26 | api.on(CONTENT_LEVEL_CHANGED, function(data){ 27 | let newQuality = data.currentQuality; 28 | if(data.type === "render"){ 29 | _.forEach( $root.find("#"+template.data.id).find(".op-setting-item").get(), function(panel){ 30 | let $panel = LA$(panel); 31 | if( $panel.find(".op-setting-item-checked").hasClass("op-show")){ 32 | $panel.find(".op-setting-item-checked").removeClass("op-show"); 33 | } 34 | if(!data.isAuto && newQuality === parseInt($panel.attr("op-data-value"))){ 35 | $panel.find(".op-setting-item-checked").addClass("op-show"); 36 | } 37 | if(data.isAuto && $panel.attr("op-data-value") === "AUTO"){ 38 | $panel.find(".op-setting-item-checked").addClass("op-show"); 39 | } 40 | }); 41 | } 42 | }, template); 43 | }; 44 | const onDestroyed = function(template){ 45 | api.off(CONTENT_LEVEL_CHANGED, null, template); 46 | }; 47 | const events = { 48 | "click .op-setting-item": function (event, $current, template) { 49 | event.preventDefault(); 50 | let value = LA$(event.currentTarget).attr("op-data-value"); 51 | if(value === "AUTO"){ 52 | api.setAutoQuality(!api.isAutoQuality()); 53 | }else{ 54 | api.setCurrentQuality(parseInt(value)); 55 | } 56 | panelManager.clear(); 57 | }, 58 | "click .op-setting-title" : function(event, $current, template){ 59 | event.preventDefault(); 60 | panelManager.removeLastItem(); 61 | } 62 | }; 63 | 64 | return OvenTemplate($container, "QualityPanel", api.getConfig(), data, events, onRendered, onDestroyed ); 65 | 66 | }; 67 | 68 | export default QualityPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/sourcePanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | 8 | 9 | const SourcePanel = function($container, api, data){ 10 | const $root = LA$(api.getContainerElement()); 11 | let panelManager = PanelManager(); 12 | 13 | data.setFront = function(isFront){ 14 | if(isFront){ 15 | $root.find("#"+data.id).removeClass("background"); 16 | }else{ 17 | $root.find("#"+data.id).addClass("background"); 18 | } 19 | }; 20 | const onRendered = function($current, template){ 21 | //Do nothing 22 | }; 23 | const onDestroyed = function(template){ 24 | //Do nothing 25 | }; 26 | const events = { 27 | "click .op-setting-item": function (event, $current, template) { 28 | event.preventDefault(); 29 | let value = LA$(event.currentTarget).attr("op-data-value"); 30 | api.setCurrentSource(parseInt(value)); 31 | panelManager.clear(); 32 | }, 33 | "click .op-setting-title" : function(event, $current, template){ 34 | event.preventDefault(); 35 | panelManager.removeLastItem(); 36 | } 37 | }; 38 | 39 | return OvenTemplate($container, "SourcePanel", api.getConfig(), data, events, onRendered, onDestroyed ); 40 | 41 | }; 42 | 43 | export default SourcePanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/speedPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | 8 | 9 | const SpeedPanel = function($container, api, data){ 10 | const $root = LA$(api.getContainerElement()); 11 | let panelManager = PanelManager(); 12 | 13 | data.setFront = function(isFront){ 14 | if(isFront){ 15 | $root.find("#"+data.id).removeClass("background"); 16 | }else{ 17 | $root.find("#"+data.id).addClass("background"); 18 | } 19 | }; 20 | const onRendered = function($current, template){ 21 | //Do nothing 22 | }; 23 | const onDestroyed = function(template){ 24 | //Do nothing 25 | }; 26 | const events = { 27 | "click .op-setting-item": function (event, $current, template) { 28 | event.preventDefault(); 29 | let value = LA$(event.currentTarget).attr("op-data-value"); 30 | api.setPlaybackRate(parseFloat(value)); 31 | panelManager.clear(); 32 | }, 33 | "click .op-setting-title" : function(event, $current, template){ 34 | event.preventDefault(); 35 | panelManager.removeLastItem(); 36 | } 37 | }; 38 | 39 | return OvenTemplate($container, "SpeedPanel", api.getConfig(), data, events, onRendered, onDestroyed ); 40 | 41 | }; 42 | 43 | export default SpeedPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/timeDisplayPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import PanelManager from "view/global/PanelManager"; 6 | import LA$ from 'utils/likeA$'; 7 | 8 | 9 | const TimeDisplayPanel = function($container, api, data){ 10 | const $root = LA$(api.getContainerElement()); 11 | let panelManager = PanelManager(); 12 | 13 | data.setFront = function(isFront){ 14 | if(isFront){ 15 | $root.find("#"+data.id).removeClass("background"); 16 | }else{ 17 | $root.find("#"+data.id).addClass("background"); 18 | } 19 | }; 20 | const onRendered = function($current, template){ 21 | //Do nothing 22 | }; 23 | const onDestroyed = function(template){ 24 | //Do nothing 25 | }; 26 | const events = { 27 | "click .op-setting-item": function (event, $current, template) { 28 | event.preventDefault(); 29 | let value = LA$(event.currentTarget).attr("op-data-value"); 30 | api.setTimecodeMode(value === "Play time"); 31 | panelManager.clear(); 32 | }, 33 | "click .op-setting-title" : function(event, $current, template){ 34 | event.preventDefault(); 35 | panelManager.removeLastItem(); 36 | } 37 | }; 38 | 39 | return OvenTemplate($container, "TimeDisplayPanel", api.getConfig(), data, events, onRendered, onDestroyed ); 40 | 41 | }; 42 | 43 | export default TimeDisplayPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/settingPanel/zoomPanel.js: -------------------------------------------------------------------------------- 1 | import OvenTemplate from 'view/engine/OvenTemplate'; 2 | import PanelManager from "view/global/PanelManager"; 3 | import LA$ from 'utils/likeA$'; 4 | 5 | 6 | const ZoomPanel = function($container, api, data){ 7 | const $root = LA$(api.getContainerElement()); 8 | const media = $root.find(".op-media-element-container"); 9 | 10 | let panelManager = PanelManager(); 11 | let zoomProperty = "--mediaZoom"; 12 | 13 | data.setFront = function(isFront){ 14 | if(isFront){ 15 | $root.find("#"+data.id).removeClass("background"); 16 | }else{ 17 | $root.find("#"+data.id).addClass("background"); 18 | } 19 | }; 20 | const onRendered = function($current, template){ 21 | //Do nothing 22 | }; 23 | const onDestroyed = function(template){ 24 | //Do nothing 25 | }; 26 | const events = { 27 | "click .op-setting-item": function (event, $current, template) { 28 | event.preventDefault(); 29 | let offset = LA$(event.currentTarget).attr("op-data-value"); 30 | let zoomFactor = api.getZoomFactor(); 31 | 32 | if (offset == 0) { 33 | zoomFactor = 1.0; 34 | } else { 35 | zoomFactor = parseFloat(zoomFactor) + parseFloat(offset); 36 | } 37 | 38 | if (zoomFactor <= 0 ) { 39 | zoomFactor = 0 40 | } 41 | 42 | media.get().style.setProperty(zoomProperty, api.setZoomFactor(parseFloat(zoomFactor).toFixed(2))); 43 | }, 44 | "click .op-setting-title" : function(event, $current, template){ 45 | event.preventDefault(); 46 | panelManager.removeLastItem(); 47 | } 48 | }; 49 | 50 | return OvenTemplate($container, "ZoomPanel", api.getConfig(), data, events, onRendered, onDestroyed ); 51 | 52 | }; 53 | 54 | export default ZoomPanel; -------------------------------------------------------------------------------- /src/js/view/components/controls/timeDisplay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 25.. 3 | */ 4 | import OvenTemplate from "view/engine/OvenTemplate"; 5 | import { naturalHms } from "utils/strings"; 6 | import { 7 | CONTENT_TIME, 8 | CONTENT_TIME_MODE_CHANGED, 9 | PROVIDER_HLS, 10 | PROVIDER_HTML5 11 | } from "api/constants"; 12 | 13 | const TimeDisplay = function ($container, api, data) { 14 | 15 | let $position = "", $duration = "", $liveBadge = "", $liveText = ""; 16 | 17 | const mediaElement = api.getMediaElement(); 18 | 19 | let hlsLive = false; 20 | let nativeHlsLive = false; 21 | 22 | let currVodPosition = 0; 23 | 24 | function convertHumanizeTime(time) { 25 | return naturalHms(time); 26 | } 27 | 28 | 29 | function getNativeHlsDvrWindow() { 30 | return mediaElement.seekable.end(mediaElement.seekable.length - 1) - mediaElement.seekable.start(0); 31 | } 32 | 33 | const onRendered = function ($current, template) { 34 | currVodPosition = 0; 35 | let isTimecode = api.isTimecodeMode(); 36 | $position = $current.find(".op-time-current"); 37 | $duration = $current.find(".op-time-duration"); 38 | $liveBadge = $current.find(".op-live-badge"); 39 | $liveText = $current.find(".op-live-text"); 40 | 41 | if (data && data.type === PROVIDER_HLS && data.duration === Infinity) { 42 | hlsLive = true; 43 | 44 | if (api.getProviderName() === PROVIDER_HTML5) { 45 | nativeHlsLive = true; 46 | } 47 | } 48 | 49 | if (data.duration !== Infinity) { 50 | 51 | if (isTimecode) { 52 | $duration.text(convertHumanizeTime(data.duration)); 53 | } else { 54 | $position.text(0); 55 | $duration.text(Math.round(data.duration * api.getFramerate()) + " (" + api.getFramerate() + "fps)"); 56 | } 57 | 58 | api.on(CONTENT_TIME_MODE_CHANGED, function (isTimecodeMode) { 59 | isTimecode = isTimecodeMode; 60 | if (isTimecode) { 61 | $position.text(convertHumanizeTime(currVodPosition)); 62 | $duration.text(convertHumanizeTime(data.duration)); 63 | } else { 64 | $position.text(Math.round(currVodPosition * api.getFramerate())); 65 | $duration.text(Math.round(data.duration * api.getFramerate()) + " (" + api.getFramerate() + "fps)"); 66 | } 67 | }, template); 68 | 69 | api.on(CONTENT_TIME, function (data) { 70 | 71 | currVodPosition = data.position; 72 | 73 | if (isTimecode) { 74 | $position.text(convertHumanizeTime(data.position)); 75 | } else { 76 | $position.text(Math.round(data.position * api.getFramerate())); 77 | } 78 | }, template); 79 | } else { 80 | if (hlsLive && !nativeHlsLive) { 81 | api.on(CONTENT_TIME, function (data) { 82 | if (!api.getConfig().legacyUI) { 83 | if (api.getMseInstance().liveSyncPosition - data.position > api.getMseInstance().targetLatency) { 84 | $liveBadge.addClass('op-live-badge-delayed'); 85 | } else { 86 | $liveBadge.removeClass('op-live-badge-delayed'); 87 | } 88 | } 89 | }, template); 90 | } else if (hlsLive && nativeHlsLive) { 91 | 92 | api.on(CONTENT_TIME, function (data) { 93 | 94 | if (!api.getConfig().legacyUI) { 95 | const dvrWindow = getNativeHlsDvrWindow(); 96 | if (dvrWindow - data.position > 3) { 97 | $liveBadge.addClass('op-live-badge-delayed'); 98 | } else { 99 | $liveBadge.removeClass('op-live-badge-delayed'); 100 | } 101 | } 102 | }, template); 103 | } 104 | } 105 | 106 | }; 107 | 108 | const onDestroyed = function (template) { 109 | api.off(CONTENT_TIME_MODE_CHANGED, null, template); 110 | api.off(CONTENT_TIME, null, template); 111 | }; 112 | const events = { 113 | "click .op-live-text": function (event, $current, template) { 114 | 115 | event.preventDefault(); 116 | 117 | if (hlsLive && !nativeHlsLive) { 118 | const syncPosition = api.getMseInstance().liveSyncPosition; 119 | api.seek(syncPosition); 120 | } 121 | } 122 | }; 123 | 124 | return OvenTemplate($container, "TimeDisplay", api.getConfig(), data, events, onRendered, onDestroyed); 125 | }; 126 | 127 | 128 | export default TimeDisplay; 129 | -------------------------------------------------------------------------------- /src/js/view/components/controls/timeDisplayTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText, data) => { 2 | return`
`+ 3 | (data.duration === Infinity 4 | ? 5 | ( 6 | `` + 7 | (data.type ==="webrtc" 8 | ? 9 | (data.isP2P ? 10 | `${uiText.controls.low_latency_p2p}` : `${uiText.controls.low_latency_live}` 11 | ) 12 | : 13 | `${uiText.controls.live}`) + 14 | `` 15 | ) 16 | :(`00:00 / 00:00`) 17 | ) + 18 | `
`; 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/view/components/controls/volumeButtonTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 20.. 3 | */ 4 | export default (uiText) => { 5 | return ( 6 | `
`+ 7 | `` + 12 | `
` + 13 | `
` + 14 | `
` + 15 | `
` + 16 | `
` + 17 | `
`+ 18 | `
` + 19 | `
` 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/js/view/components/helpers/bigButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import { 6 | STATE_IDLE, 7 | STATE_PLAYING, 8 | STATE_COMPLETE, 9 | STATE_PAUSED 10 | } from "api/constants"; 11 | import {STATE_ERROR} from "../../../api/constants"; 12 | 13 | const BigButton = function($container, api, playerState){ 14 | 15 | const onRendered = function($container, $current, template){ 16 | //Do nothing! 17 | }; 18 | const onDestroyed = function(){ 19 | //Do nothing! 20 | }; 21 | const events = { 22 | "click .op-bigbutton-container" : function(event){ 23 | event.preventDefault(); 24 | event.stopPropagation(); 25 | 26 | const currentState = api.getState(); 27 | let playlist = api.getPlaylist(); 28 | let currentPlaylistIndex = api.getCurrentPlaylist(); 29 | 30 | if (currentState === STATE_IDLE || currentState === STATE_PAUSED) { 31 | api.play(); 32 | } else if (currentState === STATE_ERROR) { 33 | api.setCurrentSource(api.getCurrentSource()); 34 | } else if(currentState === STATE_COMPLETE){ 35 | if(playlist.length === (currentPlaylistIndex+1)){ 36 | api.seek(0); 37 | api.play(); 38 | } 39 | } 40 | } 41 | }; 42 | 43 | return OvenTemplate($container, "BigButton", api.getConfig(), playerState, events, onRendered, onDestroyed ); 44 | }; 45 | 46 | export default BigButton; -------------------------------------------------------------------------------- /src/js/view/components/helpers/bigButtonTemplate.js: -------------------------------------------------------------------------------- 1 | import { 2 | STATE_IDLE, 3 | STATE_PLAYING, 4 | STATE_STALLED, 5 | STATE_LOADING, 6 | STATE_COMPLETE, 7 | STATE_PAUSED, 8 | STATE_ERROR 9 | } from "api/constants"; 10 | 11 | 12 | export default (uiText, playerState) => { 13 | return (`
` + 14 | `${playerState === STATE_PLAYING ?`` :`` }`+ 15 | `${playerState === STATE_PAUSED ?`` :`` }`+ 16 | `${playerState === STATE_IDLE ?`` :`` }`+ 17 | `${playerState === STATE_COMPLETE ?`` :`` }`+ 18 | `
`); 19 | }; -------------------------------------------------------------------------------- /src/js/view/components/helpers/captionViewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import { 6 | STATE_IDLE, 7 | STATE_PLAYING, 8 | STATE_COMPLETE, 9 | STATE_PAUSED, 10 | CONTENT_CAPTION_CHANGED, 11 | CONTENT_CAPTION_CUE_CHANGED 12 | } from "api/constants"; 13 | import LA$ from 'utils/likeA$'; 14 | 15 | 16 | const CaptionViewer = function($container, api, playerState){ 17 | const $root = LA$(api.getContainerElement()); 18 | 19 | const onRendered = function($container, $current, template){ 20 | let isDisable = false; 21 | let deleteTimer = 0; 22 | 23 | api.on(CONTENT_CAPTION_CHANGED, function(index) { 24 | if(index > -1){ 25 | isDisable = false; 26 | }else{ 27 | isDisable = true; 28 | $container.find(".op-caption-text").text(""); 29 | } 30 | }, template); 31 | 32 | api.on(CONTENT_CAPTION_CUE_CHANGED, function(data) { 33 | if(!isDisable && data && data.text){ 34 | let hideGap = data.endTime - data.startTime; 35 | 36 | if(deleteTimer){ 37 | clearTimeout(deleteTimer); 38 | } 39 | 40 | $container.find(".op-caption-text").html(data.text); 41 | 42 | if(hideGap){ 43 | deleteTimer = setTimeout(function(){ 44 | $container.find(".op-caption-text").text(""); 45 | },hideGap * 1000); 46 | } 47 | 48 | } 49 | 50 | }, template); 51 | 52 | 53 | }; 54 | const onDestroyed = function(template){ 55 | $container.find(".op-caption-text").text(""); 56 | api.off(CONTENT_CAPTION_CHANGED, null, template); 57 | api.off(CONTENT_CAPTION_CUE_CHANGED, null, template); 58 | }; 59 | const events = { 60 | }; 61 | 62 | return OvenTemplate($container, "CaptionViewer", api.getConfig(), playerState, events, onRendered, onDestroyed ); 63 | }; 64 | 65 | export default CaptionViewer; -------------------------------------------------------------------------------- /src/js/view/components/helpers/captionViewerTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 22/11/2018. 3 | */ 4 | export default (uiText) => { 5 | return ( 6 | `
` + 7 | `
` + 8 | `
` +
 9 |         `      
` + 10 | `
` 11 | ); 12 | }; -------------------------------------------------------------------------------- /src/js/view/components/helpers/contextPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 8. 1.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import LA$ from 'utils/likeA$'; 6 | 7 | const ContextPanel = function($container, api, position){ 8 | const $root = LA$(api.getContainerElement()); 9 | 10 | const onRendered = function($current, template){ 11 | const panelWidth = $current.width(); 12 | const panelHeight = $current.height(); 13 | 14 | const x = Math.min(position.pageX - $root.offset().left, $root.width() - panelWidth); 15 | const y = Math.min(position.pageY - $root.offset().top, $root.height() - panelHeight); 16 | 17 | $current.css("left" , x + "px"); 18 | $current.css("top" , y + "px"); 19 | }; 20 | const onDestroyed = function(){ 21 | //Do nothing. 22 | }; 23 | const events = { 24 | "click .op-context-item" : function(event, $current, template){ 25 | event.preventDefault(); 26 | 27 | window.open( 28 | 'https://github.com/AirenSoft/OvenPlayer', 29 | '_blank' 30 | ); 31 | } 32 | }; 33 | 34 | return OvenTemplate($container, "ContextPanel", api.getConfig(), position, events, onRendered, onDestroyed ); 35 | 36 | }; 37 | 38 | export default ContextPanel; 39 | -------------------------------------------------------------------------------- /src/js/view/components/helpers/contextPanelTemplate.js: -------------------------------------------------------------------------------- 1 | import {version} from "version"; 2 | export default (uiText) => { 3 | return ( 4 | `
` + 5 | `
` + 6 | `${uiText.context} ${version}` + 7 | `
`+ 8 | `
` 9 | ); 10 | }; -------------------------------------------------------------------------------- /src/js/view/components/helpers/mainTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 19.. 3 | */ 4 | 5 | const HelpersTemplate = function(uiText, text){ 6 | return `
`; 7 | }; 8 | 9 | export default HelpersTemplate; 10 | -------------------------------------------------------------------------------- /src/js/view/components/helpers/messageBox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import { 6 | STATE_IDLE, 7 | STATE_PLAYING, 8 | STATE_COMPLETE, 9 | STATE_PAUSED 10 | } from "api/constants"; 11 | 12 | const MessageBox = function($container, api, message, description, withTimer, iconClass, clickCallback, dontClose){ 13 | 14 | let autoDestroyTimer = ""; 15 | let data = { 16 | message : message, 17 | description : description, 18 | iconClass : iconClass, 19 | dontClose: dontClose 20 | }; 21 | 22 | 23 | const onRendered = function($current, template){ 24 | if(withTimer){ 25 | autoDestroyTimer = setTimeout(function(){ 26 | template.destroy(); 27 | }, withTimer||5000); 28 | } 29 | }; 30 | const onDestroyed = function(){ 31 | }; 32 | const events = { 33 | "click .op-message-text" : function(event, $current, template){ 34 | //event.preventDefault(); 35 | event.stopPropagation(); 36 | 37 | if (dontClose) { 38 | return; 39 | } 40 | if(autoDestroyTimer){ 41 | clearTimeout(autoDestroyTimer); 42 | } 43 | if(clickCallback){ 44 | clickCallback(); 45 | } 46 | template.destroy(); 47 | }, 48 | "click .op-con" : function(event, $current, template){ 49 | event.preventDefault(); 50 | 51 | if (dontClose) { 52 | return; 53 | } 54 | 55 | if(autoDestroyTimer){ 56 | clearTimeout(autoDestroyTimer); 57 | } 58 | 59 | if(clickCallback){ 60 | clickCallback(); 61 | } 62 | template.destroy(); 63 | } 64 | }; 65 | 66 | return OvenTemplate($container, "MessageBox", api.getConfig(), data, events, onRendered, onDestroyed ); 67 | }; 68 | 69 | 70 | export default MessageBox; -------------------------------------------------------------------------------- /src/js/view/components/helpers/messageBoxTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText, data) => { 2 | return ( 3 | `
` + 4 | `
` + 5 | `
${data.message}` + 6 | `${data.description ? `
${data.description}
` : `` }`+ 7 | `
` + 8 | `${data.iconClass ? `
` : `` }`+ 9 | `
` + 10 | `
` 11 | ); 12 | }; -------------------------------------------------------------------------------- /src/js/view/components/helpers/spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 25.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | 6 | const Spinner = function($container, api){ 7 | let $spinner = ""; 8 | 9 | const onRendered = function($current, template){ 10 | $spinner = $current; 11 | }; 12 | const onDestroyed = function(){ 13 | //Do nothing. 14 | }; 15 | const events = {}; 16 | 17 | return Object.assign(OvenTemplate($container, "Spinner", api.getConfig(), null, events, onRendered, onDestroyed ), { 18 | show: function (isShow) { 19 | if(isShow){ 20 | $spinner.show(); 21 | }else{ 22 | $spinner.hide(); 23 | } 24 | } 25 | }); 26 | }; 27 | 28 | 29 | export default Spinner; -------------------------------------------------------------------------------- /src/js/view/components/helpers/spinnerTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText) => { 2 | return `
`; 3 | }; 4 | -------------------------------------------------------------------------------- /src/js/view/components/helpers/thumbnail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 24.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import { 6 | STATE_IDLE, 7 | STATE_PLAYING, 8 | STATE_COMPLETE, 9 | STATE_PAUSED 10 | } from "api/constants"; 11 | 12 | const Thumbnail = function ($container, api, playerState) { 13 | 14 | const onRendered = function ($current, template) { 15 | 16 | }; 17 | const onDestroyed = function () { 18 | //Do nothing! 19 | }; 20 | const events = {}; 21 | 22 | return OvenTemplate($container, "Thumbnail", api.getConfig(), playerState, events, onRendered, onDestroyed); 23 | }; 24 | 25 | export default Thumbnail; -------------------------------------------------------------------------------- /src/js/view/components/helpers/thumbnailTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText, data) => { 2 | return (`
`+ 3 | `${data.title ? `
${data.title}
`:``}` + 4 | `
`); 5 | }; -------------------------------------------------------------------------------- /src/js/view/components/helpers/waterMark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sangwon Oh on 2020. 11. 10.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | import _ from 'utils/underscore'; 6 | import { 7 | STATE_IDLE, 8 | STATE_PLAYING, 9 | STATE_COMPLETE, 10 | STATE_PAUSED 11 | } from "api/constants"; 12 | 13 | const WaterMark = function($container, api, playerState){ 14 | 15 | let waterMark = null; 16 | let textElem = null; 17 | const defaultPosition = 'top-right'; 18 | const defaultX = '2.8125%'; 19 | const defaultY = '5%'; 20 | const defaultWidth = 'auto'; 21 | const defaultHeight = 'auto'; 22 | const defaultOpacity = 0.7; 23 | 24 | const onRendered = function($current, template){ 25 | 26 | waterMark = $current.find('.op-watermark'); 27 | textElem = $current.find('.op-watermark-text'); 28 | 29 | let waterMarkOption = api.getConfig().waterMark; 30 | 31 | let position = waterMarkOption.position || defaultPosition; 32 | 33 | let y = waterMarkOption.y || defaultY; 34 | let x = waterMarkOption.x || defaultX; 35 | 36 | waterMark.css(position.split('-')[0], y); 37 | waterMark.css(position.split('-')[1], x); 38 | 39 | let width = waterMarkOption.width || defaultWidth; 40 | let height = waterMarkOption.height || defaultHeight; 41 | 42 | waterMark.css('width', width); 43 | waterMark.css('height', height); 44 | 45 | let opacity = waterMarkOption.opacity || defaultOpacity; 46 | waterMark.css('opacity', opacity); 47 | 48 | if (waterMarkOption.text) { 49 | 50 | if (waterMarkOption.font) { 51 | 52 | _.each(waterMarkOption.font, function (value, key) { 53 | textElem.css(key, value); 54 | }) 55 | } 56 | } 57 | 58 | }; 59 | const onDestroyed = function(){ 60 | //Do nothing! 61 | }; 62 | const events = { 63 | 64 | }; 65 | 66 | return OvenTemplate($container, "WaterMark", api.getConfig(), playerState, events, onRendered, onDestroyed ); 67 | }; 68 | 69 | export default WaterMark; -------------------------------------------------------------------------------- /src/js/view/components/helpers/waterMarkTemplate.js: -------------------------------------------------------------------------------- 1 | export default (uiText, data) => { 2 | return (`
` + 3 | `
`+ 4 | `${data.waterMark.image ? ``:``}` + 5 | `${data.waterMark.text ? `${data.waterMark.text}`:``}` + 6 | `
` + 7 | `
`); 8 | }; -------------------------------------------------------------------------------- /src/js/view/engine/OvenTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 19.. 3 | */ 4 | 5 | import Templates from "view/engine/Templates"; 6 | import LA$ from 'utils/likeA$'; 7 | import _ from "utils/underscore"; 8 | 9 | /** 10 | * @brief This is simple ui renderer. This returns onRendered callback, onDestroyed callback on Template. And this bind events for Templates. 11 | * @param container dom element or LA$ object 12 | * @param templateName templateName 13 | * @param data preload data 14 | * @param events Template's events. 15 | * @param onRendered This callback occurs after append template. 16 | * @param onDestroyed This callback occurs after destroyed template. 17 | * @param isRoot 18 | * 19 | * */ 20 | const OvenTemplate = function (container, templateName, playerConfig, data, events, onRendered, onDestroyed, isRoot) { 21 | 22 | let $container = _.isElement(container) ? LA$(container) : container; 23 | let $template; 24 | let viewEvents = {}; 25 | let uiText = null; 26 | let that = {}; 27 | that.data = data; 28 | let createAndSelectElement = function (html) { 29 | const newElement = document.createElement('div'); 30 | newElement.innerHTML = html; 31 | 32 | $template = LA$(newElement.firstChild); 33 | 34 | return newElement.firstChild; 35 | } 36 | if(playerConfig && playerConfig.systemText){ 37 | uiText = playerConfig.systemText.ui; 38 | } 39 | if (isRoot) { 40 | $container.replace(createAndSelectElement(Templates[templateName + "Template"](uiText, data))); 41 | } else { 42 | $container.append(createAndSelectElement(Templates[templateName + "Template"](uiText, data))); 43 | } 44 | 45 | if (onRendered) { 46 | onRendered($template, that); 47 | } 48 | 49 | Object.keys(events).forEach(eventString => { 50 | let explodedText = eventString.split(" "); 51 | let eventName = explodedText[0].replace(/ /gi, ""); 52 | let target = explodedText[1].replace(/ /gi, ""); 53 | 54 | let $target = ""; 55 | 56 | if(target === "document" || target === "window" || target === "body"){ 57 | $target = LA$(target); 58 | }else{ 59 | $target = $template.find(target) || ($template.hasClass(target.replace(".","")) ? $template : null); 60 | } 61 | 62 | 63 | if (eventName && target && $target) { 64 | let id = Object.keys(viewEvents).length++; 65 | 66 | //because It retuns another data. 67 | let wrappedFunc = function (event) { 68 | return events[eventString](event, $template, that); 69 | }; 70 | viewEvents[id] = {name: eventName, target: target, callback: wrappedFunc}; 71 | 72 | /*$template.get().addEventListener(eventName,function(evt){ 73 | var gtarget = evt.target; 74 | while (gtarget!= null){ 75 | if (gtarget.isEqualNode($target.get())){ 76 | console.log("isEqual", gtarget, $target.get()); 77 | wrappedFunc(evt); 78 | return; 79 | } 80 | gtarget = gtarget.parentElement; 81 | } 82 | }, true);*/ 83 | 84 | let eventOption = null; 85 | 86 | if (eventName.indexOf('touch') > -1) { 87 | eventOption = { passive: true }; 88 | } 89 | 90 | //sometimes target is NodeList 91 | let nodeLength = $target.get().length; 92 | 93 | if(nodeLength > 1){ 94 | let nodeList = $target.get(); 95 | for(let i = 0; i < nodeLength; i ++){ 96 | nodeList[i].addEventListener(eventName, wrappedFunc, eventOption); 97 | } 98 | //IE NodeList doesn't have forEach. It's wack. 99 | //$target.get().forEach(function($item){ 100 | // $item.addEventListener(eventName, wrappedFunc); 101 | //}); 102 | }else{ 103 | $target.get().addEventListener(eventName, wrappedFunc, eventOption); 104 | } 105 | 106 | 107 | } else { 108 | return false; 109 | } 110 | }); 111 | 112 | that.destroy = function () { 113 | Object.keys(viewEvents).forEach(id => { 114 | let event = viewEvents[id]; 115 | let $target = ""; 116 | 117 | if(event.target === "document" || event.target === "window" || event.target === "body"){ 118 | $target = LA$(event.target); 119 | }else{ 120 | $target = $template.find(event.target) || ($template.hasClass(event.target.replace(".","")) ? $template : null); 121 | } 122 | 123 | //sometimes target is NodeList 124 | let nodeLength = $target.get().length; 125 | if(nodeLength > 1){ 126 | let nodeList = $target.get(); 127 | for(let i = 0; i < nodeLength; i ++){ 128 | nodeList[i].removeEventListener(event.name, event.callback); 129 | } 130 | /*$target.get().forEach(function($item){ 131 | $item.removeEventListener(event.name, event.callback); 132 | });*/ 133 | }else{ 134 | $target.get().removeEventListener(event.name, event.callback); 135 | } 136 | 137 | delete viewEvents[id]; 138 | }); 139 | 140 | if($template){ 141 | if(isRoot){ 142 | $template.removeChild(); 143 | $template.removeAttribute("class"); 144 | }else{ 145 | $template.remove(); 146 | } 147 | } 148 | 149 | if (onDestroyed) { 150 | onDestroyed(that); 151 | } 152 | }; 153 | return that; 154 | 155 | }; 156 | 157 | 158 | export default OvenTemplate; 159 | -------------------------------------------------------------------------------- /src/js/view/engine/Templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 20.. 3 | */ 4 | import TextViewTemplate from 'view/example/textviewTemplate'; 5 | import ViewTemplate from 'view/viewTemplate'; 6 | import HelpersTemplate from 'view/components/helpers/mainTemplate'; 7 | import BigButtonTemplate from 'view/components/helpers/bigButtonTemplate'; 8 | import ThumbnailTemplate from 'view/components/helpers/thumbnailTemplate'; 9 | import WaterMarkTemplate from 'view/components/helpers/waterMarkTemplate'; 10 | import MessageBoxTemplate from 'view/components/helpers/messageBoxTemplate'; 11 | import SpinnerTemplate from 'view/components/helpers/spinnerTemplate'; 12 | import ContextPanelTemplate from 'view/components/helpers/contextPanelTemplate'; 13 | import CaptionViewerTemplate from 'view/components/helpers/captionViewerTemplate'; 14 | 15 | 16 | import ControlsTemplate from 'view/components/controls/mainTemplate'; 17 | import VolumeButtonTemplate from 'view/components/controls/volumeButtonTemplate'; 18 | import ProgressBarTemplate from 'view/components/controls/progressBarTemplate'; 19 | import PlayButtonTemplate from 'view/components/controls/playButtonTemplate'; 20 | import SettingButtonTemplate from 'view/components/controls/settingButtonTemplate'; 21 | import FrameButtonsTemplate from 'view/components/controls/frameButtonsTemplate'; 22 | import TimeDisplayTemplate from 'view/components/controls/timeDisplayTemplate'; 23 | import FullScreenButtonTemplate from 'view/components/controls/fullScreenButtonTemplate'; 24 | import PanelsTemplate from 'view/components/controls/settingPanel/mainTemplate'; 25 | import SpeedPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 26 | import ZoomPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 27 | import SourcePanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 28 | import QualityPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 29 | import AudioTrackPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 30 | import CaptionPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 31 | import TimeDisplayPanelTemplate from 'view/components/controls/settingPanel/mainTemplate'; 32 | import PlaylistPanelTemplate from 'view/components/controls/playlistPanelTemplate'; 33 | 34 | const Templates = { 35 | TextViewTemplate, 36 | ViewTemplate, 37 | HelpersTemplate, 38 | BigButtonTemplate, 39 | ThumbnailTemplate, 40 | WaterMarkTemplate, 41 | MessageBoxTemplate, 42 | SpinnerTemplate, 43 | ContextPanelTemplate, 44 | CaptionViewerTemplate, 45 | 46 | ControlsTemplate, 47 | VolumeButtonTemplate, 48 | ProgressBarTemplate, 49 | PlayButtonTemplate, 50 | SettingButtonTemplate, 51 | FrameButtonsTemplate, 52 | TimeDisplayTemplate, 53 | FullScreenButtonTemplate, 54 | PanelsTemplate, 55 | SpeedPanelTemplate, 56 | ZoomPanelTemplate, 57 | SourcePanelTemplate, 58 | QualityPanelTemplate, 59 | AudioTrackPanelTemplate, 60 | CaptionPanelTemplate, 61 | TimeDisplayPanelTemplate, 62 | PlaylistPanelTemplate 63 | }; 64 | 65 | export default Templates; -------------------------------------------------------------------------------- /src/js/view/example/textview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 19.. 3 | */ 4 | import OvenTemplate from 'view/engine/OvenTemplate'; 5 | 6 | const TextView = function($container, api, text){ 7 | const onRendered = function($current, template){ 8 | 9 | }; 10 | const onDestroyed = function(){ 11 | //Do nothing. 12 | }; 13 | const events = { 14 | "click .btn" : function(event, $current, template){ 15 | event.preventDefault(); 16 | alert("Hi!"); 17 | } 18 | }; 19 | 20 | return OvenTemplate($container, "TextView", text, events, onRendered, onDestroyed ); 21 | 22 | }; 23 | 24 | export default TextView; -------------------------------------------------------------------------------- /src/js/view/example/textviewTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 19.. 3 | */ 4 | 5 | const TextViewTemplate = function(text){ 6 | return `
` + 7 | `

${text}

` + 8 | `` + 9 | `
`; 10 | }; 11 | 12 | export default TextViewTemplate; -------------------------------------------------------------------------------- /src/js/view/global/PanelManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 26.. 3 | */ 4 | import _ from "utils/underscore"; 5 | 6 | let settingPanelList = []; 7 | 8 | const PanelManager = function(){ 9 | const that = {}; 10 | 11 | let refreshFront = function(){ 12 | for(let i = 0 ; i < settingPanelList.length; i ++){ 13 | settingPanelList[i].data.setFront(false); 14 | } 15 | if(settingPanelList.length ){ 16 | settingPanelList[settingPanelList.length - 1].data.setFront(true); 17 | } 18 | }; 19 | that.clear = () => { 20 | //clear all SettingPanelTemplate 21 | 22 | _.each(settingPanelList, function(settingPanel){ 23 | settingPanel.destroy(); 24 | }); 25 | settingPanelList = []; 26 | refreshFront(); 27 | }; 28 | 29 | that.removeLastItem = () =>{ 30 | let last = settingPanelList.pop(); 31 | last.destroy(); 32 | refreshFront(); 33 | }; 34 | 35 | that.add = (settingPanelObject) => { 36 | settingPanelList.push(settingPanelObject); 37 | refreshFront(); 38 | }; 39 | 40 | that.size = () => { 41 | return settingPanelList.length; 42 | }; 43 | 44 | return that; 45 | }; 46 | 47 | export default PanelManager; -------------------------------------------------------------------------------- /src/js/view/viewTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hoho on 2018. 7. 20.. 3 | */ 4 | 5 | const ViewTemplate = function (uiText, id) { 6 | return `
` + 7 | `
` + 8 | `
` + 9 | `
` + 10 | `
` + 11 | `
` + 12 | `
` + 13 | `
` + 14 | `
` 15 | }; 16 | export default ViewTemplate; 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const packageInfo = require('./package.json'); 4 | 5 | const config = { 6 | mode: 'production', 7 | entry: { 8 | 'ovenplayer': ['core-js/stable', 'whatwg-fetch', './src/js/ovenplayer.js'] 9 | }, 10 | target: ['web', 'es5'], 11 | resolve: { 12 | modules: [ 13 | path.resolve(__dirname, 'src/js'), 14 | path.resolve('./node_modules') 15 | ] 16 | }, 17 | output: { 18 | filename: '[name].js', 19 | path: path.resolve(__dirname, 'dist'), 20 | library: { 21 | name: 'OvenPlayer', 22 | type: 'umd', 23 | export: 'default' 24 | }, 25 | clean: true 26 | }, 27 | devtool: 'source-map', 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | exclude: /(node_modules)/, 33 | use: [ 34 | { 35 | loader: 'babel-loader', 36 | options: { 37 | presets: [ 38 | [ 39 | '@babel/preset-env', 40 | { 41 | 'useBuiltIns': 'entry', 42 | 'corejs': 3, 43 | 'targets': { 'ie': '11' } 44 | } 45 | ] 46 | ] 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | test: /\.less$/i, 53 | use: [ 54 | { 55 | loader: 'style-loader', 56 | }, 57 | { 58 | loader: 'css-loader', 59 | }, 60 | { 61 | loader: 'less-loader', 62 | options: { 63 | lessOptions: { 64 | compress: true, 65 | strictMath: true 66 | } 67 | } 68 | }, 69 | ], 70 | }, 71 | { 72 | test: /\.(ttf|eot|svg|gif)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 73 | type: 'asset/inline' 74 | }, 75 | { 76 | test: /\.worker\.js$/, 77 | use: { loader: "worker-loader" } 78 | }, 79 | ] 80 | }, 81 | plugins: [ 82 | new webpack.DefinePlugin({ 83 | __VERSION__: `'${packageInfo.version}'` 84 | }) 85 | ] 86 | }; 87 | 88 | module.exports = config; -------------------------------------------------------------------------------- /webpack.development.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.js') 2 | const path = require('path'); 3 | 4 | console.log(config) 5 | 6 | config.mode = 'development'; 7 | config.output.path = path.resolve(__dirname, 'dev'); 8 | 9 | module.exports = config; --------------------------------------------------------------------------------