├── .babelrc ├── .eslintrc.yml ├── .github └── workflows │ ├── build.yml │ ├── nodejs.yml │ └── publish.yml ├── .gitignore ├── .not_used_travis.yml ├── .npmignore ├── README.md ├── demo ├── demo.css ├── demo.js └── index.html ├── icons ├── eyevinnlogo.png ├── media-pause.svg ├── media-play.svg ├── volume-high.svg └── volume-off.svg ├── index.js ├── jest.config.js ├── main.js ├── package-lock.json ├── package.json ├── src ├── player.js ├── playerTechs │ ├── dash_player.js │ ├── hls_player.js │ ├── interface.js │ └── mss_player.js ├── player_skin.js ├── player_tech_factory.js └── utils │ └── constants.js └── styles ├── main.scss └── skin.scss /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | jest/globals: true 5 | 6 | extends: 7 | - eslint-config-prettier 8 | - eslint:recommended 9 | 10 | plugins: 11 | - prettier 12 | - jest 13 | 14 | globals: 15 | Promise: true 16 | 17 | rules: 18 | prettier/prettier: 19 | - error 20 | arrow-body-style: off 21 | no-console: off 22 | 23 | 24 | parser: babel-eslint 25 | 26 | parserOptions: 27 | sourceType: module 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy bundle 2 | on: 3 | create: 4 | tags: '*' 5 | 6 | jobs: 7 | package: 8 | name: Build and deploy bundle to S3 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Build 16 | run: | 17 | npm install 18 | npm run build 19 | 20 | - name: Configure AWS credentials 21 | uses: aws-actions/configure-aws-credentials@v1 22 | with: 23 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 24 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 25 | aws-region: eu-west-1 26 | 27 | - name: Get the version 28 | id: get_version 29 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) 30 | 31 | - name: Upload bundle to S3 32 | run: | 33 | aws s3 cp ./build/ s3://origin-player-releases/${{ steps.get_version.outputs.VERSION }}/build/ --recursive -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | node-version: [12.x, 14.x] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: npm install, build and test 19 | run: | 20 | npm ci 21 | npm run build --if-present 22 | npm test -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm install 18 | - run: npm run build-npm 19 | - run: npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.gitignore.io/api/macos,visualstudiocode,node 4 | # Edit at https://www.gitignore.io/?templates=macos,visualstudiocode,node 5 | 6 | ### macOS ### 7 | # General 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Node ### 35 | # Logs 36 | logs 37 | *.log 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | lerna-debug.log* 42 | 43 | # Diagnostic reports (https://nodejs.org/api/report.html) 44 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 45 | 46 | # Runtime data 47 | pids 48 | *.pid 49 | *.seed 50 | *.pid.lock 51 | 52 | # Directory for instrumented libs generated by jscoverage/JSCover 53 | lib-cov 54 | 55 | # Coverage directory used by tools like istanbul 56 | coverage 57 | *.lcov 58 | 59 | # nyc test coverage 60 | .nyc_output 61 | 62 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 63 | .grunt 64 | 65 | # Bower dependency directory (https://bower.io/) 66 | bower_components 67 | 68 | # node-waf configuration 69 | .lock-wscript 70 | 71 | # Compiled binary addons (https://nodejs.org/api/addons.html) 72 | build/Release 73 | 74 | # Dependency directories 75 | node_modules/ 76 | jspm_packages/ 77 | 78 | # TypeScript v1 declaration files 79 | typings/ 80 | 81 | # TypeScript cache 82 | *.tsbuildinfo 83 | 84 | # Optional npm cache directory 85 | .npm 86 | 87 | # Optional eslint cache 88 | .eslintcache 89 | 90 | # Optional REPL history 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | *.tgz 95 | 96 | # Yarn Integrity file 97 | .yarn-integrity 98 | 99 | # dotenv environment variables file 100 | .env 101 | .env.test 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | .cache 105 | 106 | # next.js build output 107 | .next 108 | 109 | # nuxt.js build output 110 | .nuxt 111 | 112 | # react / gatsby 113 | public/ 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | ### VisualStudioCode ### 128 | .vscode/* 129 | !.vscode/settings.json 130 | !.vscode/tasks.json 131 | !.vscode/launch.json 132 | !.vscode/extensions.json 133 | 134 | ### VisualStudioCode Patch ### 135 | # Ignore all local history of files 136 | .history 137 | 138 | # End of https://www.gitignore.io/api/macos,visualstudiocode,node 139 | 140 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 141 | 142 | dist/ 143 | pkg/ 144 | build/ 145 | .cache/ 146 | .vscode/ 147 | -------------------------------------------------------------------------------- /.not_used_travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - '8' 6 | script: npm run build 7 | before_deploy: rm -rf node_modules 8 | deploy: 9 | on: 10 | tags: true 11 | provider: s3 12 | access_key_id: $AWS_ACCESS_KEY_ID 13 | secret_access_key: $AWS_SECRET_ACCESS_KEY 14 | bucket: origin-player-releases 15 | region: eu-west-1 16 | upload_dir: $TRAVIS_TAG 17 | skip_cleanup: true 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .github 3 | build 4 | dist 5 | demo 6 | node_modules 7 | .not_used_travis-yml 8 | jest.config.js 9 | .babelrc 10 | .eslintc.yml 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eyevinn HTML Player 2 | 3 | Eyevinn HTML Player is a simplistic video player for playback of ABR streams. It is free-to-use and currently supports the ABR streaming formats Apple HLS, MPEG-DASH and Microsoft Smooth Streaming. 4 | 5 | Demo implementation available [here](https://player.eyevinn.technology/) 6 | 7 | ![screenshot](https://player.eyevinn.technology/screenshot.png) 8 | 9 | The player is built on [Hls.js](https://video-dev.github.io/hls.js/), [Shaka Player](https://github.com/google/shaka-player) and [DashJs](https://github.com/Dash-Industry-Forum/dash.js/) 10 | 11 | # Getting Started 12 | 13 | The player is available both from CDN as well as from NPM for building into your JavaScript application. 14 | 15 | ## CDN 16 | 17 | To be able to quickly add Eyevinn HTML Player to your project we are hosting the player and delivered through our CDN. Want to use a package manager and host the player on your site head over to the [download section](https://github.com/Eyevinn/html-player/releases). 18 | 19 | ### JS 20 | 21 | Place the following ` 25 | ``` 26 | 27 | ### CSS 28 | 29 | Copy-paste the stylesheet `` into your `` to load the CSS for the player. 30 | 31 | ``` 32 | 33 | ``` 34 | 35 | ### Template 36 | 37 | The snippet below shows an example on how to implement the player: 38 | 39 | ```html 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | ``` 62 | 63 | ## NPM 64 | 65 | ### JS 66 | 67 | Install with `npm install @eyevinn/html-player`; 68 | 69 | Include in your project by calling `import { setupEyevinnPlayer } from "@eyevinn/html-player"`. 70 | 71 | ### CSS 72 | 73 | Include in your JavaScript file by calling `import "@eyevinn/html-player/pkg/style.css"`; 74 | 75 | ### Example 76 | 77 | ```html 78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | ```js 90 | import { setupEyevinnPlayer } from "@eyevinn/html-player"; 91 | import "@eyevinn/html-player/pkg/style.css"; 92 | 93 | document.addEventListener('DOMContentLoaded', function(event) { 94 | setupEyevinnPlayer("videoContainer", "source.m3u8").then((player) => { 95 | window.myPlayerInstance = player; 96 | player.play(); 97 | }); 98 | }); 99 | ``` 100 | 101 | To kill it later on 102 | 103 | ```js 104 | window.myPlayerInstance.destroy(); 105 | ``` 106 | 107 | ## Contribution 108 | 109 | You are welcome to either contribute to this project or spin-off a fork of your own. This code is released under the Apache 2.0 license. 110 | 111 | ``` 112 | Copyright 2018 Eyevinn Technology 113 | 114 | Licensed under the Apache License, Version 2.0 (the "License"); 115 | you may not use this file except in compliance with the License. 116 | You may obtain a copy of the License at 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | 120 | Unless required by applicable law or agreed to in writing, software 121 | distributed under the License is distributed on an "AS IS" BASIS, 122 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 123 | See the License for the specific language governing permissions and 124 | limitations under the License. 125 | ``` 126 | 127 | ## About Eyevinn Technology 128 | 129 | Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor. 130 | 131 | At Eyevinn, every software developer consultant has a dedicated budget reserved for open source development and contribution to the open source community. This give us room for innovation, team building and personal competence development. And also gives us as a company a way to contribute back to the open source community. 132 | 133 | Want to know more about Eyevinn and how it is to work here. Contact us at work@eyevinn.se! 134 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | max-width: 1024px; 3 | max-height: 576px; 4 | margin: 0 auto; 5 | background-color: black; 6 | margin-top: 5px; 7 | margin-bottom: 5px; 8 | } 9 | .demo h1 { 10 | text-align: center; 11 | font-family: Arial, Verdana; 12 | font-size: 34pt; 13 | } 14 | .demo h2 { 15 | text-align: center; 16 | font-family: Arial, Verdana; 17 | font-size: 24pt; 18 | } 19 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | /* global setupEyevinnPlayer */ 2 | document.addEventListener("DOMContentLoaded", function() { 3 | setupEyevinnPlayer( 4 | "hls-wrapper", 5 | "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" 6 | ) 7 | .then(function(player) { 8 | window.hlsPlayer = player; 9 | console.log("HLS Player initiated: " + player.version); 10 | player.play(false); 11 | console.log("HLS: isLive=" + player.isLive); 12 | }) 13 | .catch(console.error); 14 | setupEyevinnPlayer( 15 | "dash-wrapper", 16 | "https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-only/dash.mpd", 17 | { skin: false } 18 | ) 19 | .then(function(player) { 20 | window.dashPlayer = player; 21 | console.log("Dash Player initiated: " + player.version); 22 | player.play(true); 23 | console.log("DASH: isLive=" + player.isLive); 24 | }) 25 | .catch(console.error); 26 | setupEyevinnPlayer( 27 | "mss-wrapper", 28 | "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest" 29 | ) 30 | .then(function(player) { 31 | window.mssPlayer = player; 32 | console.log("MSS Player initiated: " + player.version); 33 | player.play(true); 34 | console.log("MSS: isLive=" + player.isLive); 35 | }) 36 | .catch(console.error); 37 | }); 38 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Eyevinn HTML Player 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Eyevinn HTML Player Demo

14 |

HLS

15 |
16 |

MPEG-DASH

17 |
18 |

MSS

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /icons/eyevinnlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eyevinn/html-player/50248f00995610f89ac71895ea1bf7321c9d6cb9/icons/eyevinnlogo.png -------------------------------------------------------------------------------- /icons/media-pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/media-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icons/volume-high.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /icons/volume-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { setupEyevinnPlayer } from "./main.js"; 2 | 3 | window.setupEyevinnPlayer = setupEyevinnPlayer; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv-vars"); 2 | /* eslint-disable */ 3 | module.exports = { 4 | testMatch: ["**/tests/**/*.js?(x)"], 5 | coverageDirectory: "./coverage", 6 | coverageReporters: [ 7 | "lcov" 8 | ], 9 | collectCoverageFrom: [ 10 | "**/*.{js,jsx}", 11 | // Non-library folders/files 12 | "!**/node_modules/**", 13 | "!**/coverage/**", 14 | "!jest.config.js" 15 | ], 16 | globals: { "__VERSION__": "1.0.0" } 17 | }; 18 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import { Player } from "./src/player"; 2 | import { DEFAULT_OPTIONS } from "./src/utils/constants"; 3 | 4 | export const setupEyevinnPlayer = async function(wrapperId, manifestUrl, opts) { 5 | const options = Object.assign({}, DEFAULT_OPTIONS, opts); 6 | const player = new Player(wrapperId, manifestUrl, options); 7 | return await player.init(); 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eyevinn/html-player", 3 | "version": "0.6.1", 4 | "description": "Eyevinn HTML5 Player with support for HLS, MPEG-DASH and MSS", 5 | "main": "./pkg/eyevinn-html-player", 6 | "scripts": { 7 | "build-js": "parcel build index.js --no-source-maps --out-dir build --out-file eyevinn-html-player.js", 8 | "build-css": "parcel build styles/main.scss --no-source-maps --public-url ./ --out-dir build --out-file eyevinn-html-player.css", 9 | "build": "npm run build-js && npm run build-css", 10 | "build-npm-js": "parcel build main.js --no-source-maps --out-dir pkg --out-file eyevinn-html-player.js", 11 | "build-npm-css": "parcel build styles/main.scss --no-source-maps --public-url ./ --out-dir pkg --out-file style.css", 12 | "build-npm": "npm run build-npm-js && npm run build-npm-css", 13 | "start": "parcel ./demo/index.html", 14 | "test": "jest --passWithNoTests", 15 | "postversion": "git push && git push --tags" 16 | }, 17 | "author": "Jonas Birmé ", 18 | "contributors": [ 19 | "Erik Hoffman ", 20 | "Benjamin Wallberg " 21 | ], 22 | "license": "Apache-2.0", 23 | "dependencies": { 24 | "dashjs": "^3.1.2", 25 | "hls.js": "^0.14.6", 26 | "shaka-player": "^3.0.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.8.3", 30 | "@babel/core": "^7.8.3", 31 | "@babel/plugin-transform-runtime": "^7.8.3", 32 | "@babel/preset-env": "^7.8.3", 33 | "@types/jest": "^24.0.23", 34 | "babel-eslint": "^10.0.3", 35 | "babel-jest": "^25.5.1", 36 | "babel-loader": "^8.1.0", 37 | "dotenv-vars": "^2.1.0", 38 | "eslint": "^6.8.0", 39 | "eslint-config-prettier": "^6.7.0", 40 | "eslint-plugin-jest": "^23.1.1", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "jest": "^25.5.2", 43 | "parcel-bundler": "^1.12.4", 44 | "prettier": "^1.19.1", 45 | "sass": "^1.26.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/player.js: -------------------------------------------------------------------------------- 1 | import { PlayerTechFactory } from "./player_tech_factory"; 2 | import { PlayerSkin } from "./player_skin"; 3 | 4 | export class Player { 5 | constructor(wrapperId, manifestUrl, options) { 6 | this.wrapperId_ = wrapperId; 7 | this.manifestUrl_ = manifestUrl; 8 | this.options_ = options; 9 | this.playerTechFactory_ = new PlayerTechFactory( 10 | this.wrapperId_, 11 | this.manifestUrl_ 12 | ); 13 | } 14 | 15 | async init() { 16 | const player = await this.playerTechFactory_.constructPlayerTech(); 17 | if (this.options_.skin) { 18 | const skin = new PlayerSkin(player); 19 | skin.init(); 20 | player.attachControllerSkin(skin); 21 | } 22 | return player; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/playerTechs/dash_player.js: -------------------------------------------------------------------------------- 1 | import { PlayerTechInterface } from "./interface"; 2 | import * as Shaka from "shaka-player"; 3 | 4 | export class DashPlayer extends PlayerTechInterface { 5 | constructor(wrapperId, manifestUrl) { 6 | super(wrapperId, manifestUrl); 7 | } 8 | 9 | load() { 10 | return new Promise(resolve => { 11 | let shakap = new Shaka.Player(this.videoElement_); 12 | shakap.load(this.manifestUrl_).then(() => { 13 | resolve(); 14 | }); 15 | this.shakap_ = shakap; 16 | }); 17 | } 18 | 19 | get isLive() { 20 | return this.shakap_.isLive(); 21 | } 22 | 23 | destroy() { 24 | if (this.shakap_) { 25 | this.shakap_.destroy(); 26 | } 27 | super.destroy(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/playerTechs/hls_player.js: -------------------------------------------------------------------------------- 1 | import { PlayerTechInterface } from "./interface"; 2 | import Hls from "hls.js"; 3 | 4 | export class HlsPlayer extends PlayerTechInterface { 5 | constructor(wrapperId, manifestUrl) { 6 | super(wrapperId, manifestUrl); 7 | } 8 | 9 | load() { 10 | return new Promise(resolve => { 11 | let hls = new Hls(); 12 | 13 | if (this.videoElement_.canPlayType("application/vnd.apple.mpegurl")) { 14 | this.videoElement_.src = this.manifestUrl_; 15 | resolve(); 16 | } else if (Hls.isSupported) { 17 | hls.attachMedia(this.videoElement_); 18 | hls.on(Hls.Events.MEDIA_ATTACHED, () => { 19 | hls.loadSource(this.manifestUrl_); 20 | }); 21 | hls.on(Hls.Events.MANIFEST_PARSED, () => { 22 | //resolve(); 23 | }); 24 | hls.on(Hls.Events.LEVEL_LOADED, (event, data) => { 25 | this.isLive_ = data.details.live; 26 | resolve(); 27 | }); 28 | this.hls_ = hls; 29 | } 30 | }); 31 | } 32 | 33 | get isLive() { 34 | if (this.hls_) { 35 | return this.isLive_; 36 | } else { 37 | return isNaN(this.videoElement_.duration); 38 | } 39 | } 40 | 41 | destroy() { 42 | if (this.hls_) { 43 | this.hls_.destroy(); 44 | } 45 | super.destroy(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/playerTechs/interface.js: -------------------------------------------------------------------------------- 1 | const VERSION = require("../../package.json").version; 2 | 3 | export class PlayerTechInterface { 4 | constructor(wrapperId, manifestUrl) { 5 | this.manifestUrl_ = manifestUrl; 6 | this.videoElement_ = null; 7 | this.wrapperElement_ = this.init_(wrapperId); 8 | 9 | this.eventListeners_ = { 10 | playing: [], 11 | paused: [], 12 | muted: [], 13 | unmuted: [] 14 | }; 15 | } 16 | 17 | get version() { 18 | return VERSION; 19 | } 20 | 21 | get wrapper() { 22 | return this.wrapperElement_; 23 | } 24 | 25 | get isPlaying() { 26 | return !this.videoElement_.paused; 27 | } 28 | 29 | get isMuted() { 30 | return this.videoElement_.muted; 31 | } 32 | 33 | get isLive() { 34 | throw new Error( 35 | "Missing implementation of isLive() property in player tech." 36 | ); 37 | } 38 | 39 | get duration() { 40 | if (!this.isLive) { 41 | return this.videoElement_.duration; 42 | } 43 | return NaN; 44 | } 45 | 46 | get position() { 47 | if (!this.isLive) { 48 | return this.videoElement_.currentTime; 49 | } 50 | return NaN; 51 | } 52 | 53 | set position(newpos) { 54 | if (!this.isLive) { 55 | this.videoElement_.currentTime = newpos; 56 | } 57 | } 58 | 59 | on(event, func) { 60 | this.eventListeners_[event].push(func); 61 | } 62 | 63 | attachControllerSkin(skin) { 64 | this.controllerSkin_ = skin; 65 | } 66 | 67 | play(startMuted) { 68 | if (startMuted) { 69 | this.videoElement_.muted = true; 70 | } else { 71 | this.videoElement_.muted = false; 72 | } 73 | let evname = this.videoElement_.muted ? "muted" : "unmuted"; 74 | for (let f of this.eventListeners_[evname]) { 75 | f(); 76 | } 77 | let playPromise = this.videoElement_.play(); 78 | 79 | if (playPromise !== undefined) { 80 | playPromise 81 | .catch(() => { 82 | console.log("Auto-play was prevented, show big play button"); 83 | this.controllerSkin_.showBigPlayButton(); 84 | }) 85 | .then(() => {}); 86 | } 87 | } 88 | 89 | pause() { 90 | this.videoElement_.pause(); 91 | } 92 | 93 | mute() { 94 | this.videoElement_.muted = true; 95 | } 96 | 97 | unmute() { 98 | this.videoElement_.muted = false; 99 | } 100 | 101 | load() { 102 | return new Promise((resolve, reject) => { 103 | reject("Missing implementation of load() in player tech."); 104 | }); 105 | } 106 | 107 | init_(wrapperId) { 108 | this.videoElement_ = document.createElement("video"); 109 | this.videoElement_.className = "eyevinn-player"; 110 | this.videoElement_.setAttribute("playsinline", "playsinline"); 111 | let wrapperElement = document.getElementById(wrapperId); 112 | wrapperElement.appendChild(this.videoElement_); 113 | wrapperElement.style.setProperty("position", "relative"); 114 | 115 | this.videoElement_.addEventListener("playing", () => { 116 | for (let f of this.eventListeners_["playing"]) { 117 | f(); 118 | } 119 | }); 120 | 121 | this.videoElement_.addEventListener("pause", () => { 122 | for (let f of this.eventListeners_["paused"]) { 123 | f(); 124 | } 125 | }); 126 | 127 | this.videoElement_.addEventListener("volumechange", () => { 128 | let evname = this.videoElement_.muted ? "muted" : "unmuted"; 129 | for (let f of this.eventListeners_[evname]) { 130 | f(); 131 | } 132 | }); 133 | 134 | return wrapperElement; 135 | } 136 | 137 | destroy() { 138 | console.log("Player destroyed"); 139 | this.videoElement_.remove(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/playerTechs/mss_player.js: -------------------------------------------------------------------------------- 1 | import { PlayerTechInterface } from "./interface"; 2 | import { MediaPlayer } from "dashjs"; 3 | // eslint-disable-next-line no-unused-vars 4 | const mss = require("dashjs/build/es5/src/mss"); 5 | 6 | export class MssPlayer extends PlayerTechInterface { 7 | constructor(wrapperId, manifestUrl) { 8 | super(wrapperId, manifestUrl); 9 | } 10 | 11 | load() { 12 | return new Promise((resolve, reject) => { 13 | let mediaPlayer = MediaPlayer().create(); 14 | mediaPlayer.initialize(); 15 | mediaPlayer.attachView(this.videoElement_); 16 | mediaPlayer.attachSource(this.manifestUrl_); 17 | mediaPlayer.on(MediaPlayer.events.MANIFEST_LOADED, () => { 18 | resolve(); 19 | }); 20 | mediaPlayer.on(MediaPlayer.events.ERROR, ev => { 21 | reject(`Failed to load Mss Player: ${ev.error.message}`); 22 | }); 23 | this.mediaPlayer_ = mediaPlayer; 24 | }); 25 | } 26 | 27 | get isLive() { 28 | return this.mediaPlayer_.isDynamic(); 29 | } 30 | 31 | destroy() { 32 | if (this.mediaPlayer_) { 33 | this.mediaPlayer_.reset(); 34 | } 35 | super.destroy(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/player_skin.js: -------------------------------------------------------------------------------- 1 | export class PlayerSkin { 2 | constructor(playerInterface) { 3 | this.playerInterface_ = playerInterface; 4 | } 5 | 6 | init() { 7 | return this.setupControllers_(this.playerInterface_.wrapper); 8 | } 9 | 10 | showBigPlayButton() { 11 | this.bigPlayButton_.className = "player-btn-big player-btn-big-visible"; 12 | } 13 | 14 | setupControllers_(wrapperElement) { 15 | let controllerElement = document.createElement("div"); 16 | controllerElement.className = "player-controller player-controller-hidden"; 17 | 18 | let logoElement = document.createElement("div"); 19 | logoElement.className = "player-logo"; 20 | controllerElement.appendChild(logoElement); 21 | 22 | if (!this.playerInterface_.isLive) { 23 | controllerElement.appendChild(this.setupTimeline_()); 24 | } 25 | 26 | let btnTogglePlay = document.createElement("div"); 27 | btnTogglePlay.className = "player-btn-toggle-play player-btn-play"; 28 | btnTogglePlay.addEventListener("mouseover", event => { 29 | event.target.className += " player-btn-hover"; 30 | }); 31 | btnTogglePlay.addEventListener("mouseout", event => { 32 | event.target.className = event.target.className.replace( 33 | "player-btn-hover", 34 | "" 35 | ); 36 | }); 37 | btnTogglePlay.addEventListener("click", () => { 38 | if (!this.playerInterface_.isPlaying) { 39 | this.playerInterface_.play(); 40 | } else { 41 | this.playerInterface_.pause(); 42 | } 43 | }); 44 | controllerElement.appendChild(btnTogglePlay); 45 | 46 | let bigPlayButton = document.createElement("div"); 47 | bigPlayButton.className = "player-btn-big player-btn-big-hidden"; 48 | wrapperElement.appendChild(bigPlayButton); 49 | this.bigPlayButton_ = bigPlayButton; 50 | this.bigPlayButton_.addEventListener("click", () => { 51 | this.playerInterface_.play(); 52 | }); 53 | 54 | let btnToggleAudio = document.createElement("div"); 55 | btnToggleAudio.className = "player-btn-toggle-audio player-btn-audio-on"; 56 | btnToggleAudio.addEventListener("mouseover", event => { 57 | event.target.className += " player-btn-hover"; 58 | }); 59 | btnToggleAudio.addEventListener("mouseout", event => { 60 | event.target.className = event.target.className.replace( 61 | "player-btn-hover", 62 | "" 63 | ); 64 | }); 65 | btnToggleAudio.addEventListener("click", () => { 66 | if (!this.playerInterface_.isMuted) { 67 | this.playerInterface_.mute(); 68 | } else { 69 | this.playerInterface_.unmute(); 70 | } 71 | }); 72 | controllerElement.appendChild(btnToggleAudio); 73 | 74 | this.playerInterface_.on("playing", () => { 75 | btnTogglePlay.className = "player-btn-toggle-play player-btn-pause"; 76 | this.bigPlayButton_.className = "player-btn-big player-btn-big-hidden"; 77 | }); 78 | this.playerInterface_.on("paused", () => { 79 | btnTogglePlay.className = "player-btn-toggle-play player-btn-play"; 80 | }); 81 | this.playerInterface_.on("muted", () => { 82 | btnToggleAudio.className = "player-btn-toggle-audio player-btn-audio-off"; 83 | }); 84 | this.playerInterface_.on("unmuted", () => { 85 | btnToggleAudio.className = "player-btn-toggle-audio player-btn-audio-on"; 86 | }); 87 | 88 | wrapperElement.appendChild(controllerElement); 89 | 90 | wrapperElement.addEventListener("mousemove", () => { 91 | controllerElement.className = 92 | "player-controller player-controller-visible"; 93 | 94 | this.controllerTimer = setTimeout(() => { 95 | controllerElement.className = 96 | "player-controller player-controller-hidden"; 97 | }, 5000); 98 | }); 99 | } 100 | 101 | setupTimeline_() { 102 | let timelineContainer = document.createElement("div"); 103 | timelineContainer.className = "player-timeline-container"; 104 | let timelineElement = document.createElement("div"); 105 | timelineElement.className = "player-timeline"; 106 | let durationElement = document.createElement("div"); 107 | durationElement.className = "player-timeline-duration"; 108 | let positionElement = document.createElement("div"); 109 | positionElement.className = "player-timeline-position"; 110 | 111 | timelineContainer.appendChild(positionElement); 112 | timelineContainer.appendChild(timelineElement); 113 | timelineContainer.appendChild(durationElement); 114 | 115 | this.timelineUpdateTimer = setInterval(() => { 116 | durationElement.innerHTML = this.formatPlayerTime_( 117 | this.playerInterface_.duration 118 | ); 119 | // let w = timelineElement.clientWidth; 120 | let pos = this.playerInterface_.position; 121 | positionElement.innerHTML = this.formatPlayerTime_(pos); 122 | let progress = (pos / this.playerInterface_.duration) * 100; // percentage 123 | timelineElement.setAttribute( 124 | "style", 125 | `background: linear-gradient(90deg, #0FBAF0 ${progress}%, #000000 ${progress}%)` 126 | ); 127 | }, 1000); 128 | 129 | timelineElement.addEventListener("click", ev => { 130 | let w = ev.target.clientWidth; 131 | let position = (ev.offsetX / w) * this.playerInterface_.duration; 132 | //console.log(`${(ev.offsetX / w) * 100}% position=${position}`); 133 | this.playerInterface_.position = position; 134 | }); 135 | 136 | return timelineContainer; 137 | } 138 | 139 | formatPlayerTime_(secs) { 140 | let sec = parseInt(secs, 10); 141 | let h = Math.floor(sec / 3600) % 24; 142 | let m = Math.floor(sec / 60) % 60; 143 | let s = sec % 60; 144 | return [h, m, s] 145 | .map(v => (v < 10 ? "0" + v : v)) 146 | .filter((v, i) => v !== "00" || i > 0) 147 | .join(":"); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/player_tech_factory.js: -------------------------------------------------------------------------------- 1 | import { HlsPlayer } from "./playerTechs/hls_player"; 2 | import { DashPlayer } from "./playerTechs/dash_player"; 3 | import { MssPlayer } from "./playerTechs/mss_player"; 4 | import { 5 | CONTENT_TYPE_MAP, 6 | ENUM_TYPE_HLS, 7 | ENUM_TYPE_MPEGDASH, 8 | ENUM_TYPE_MSS 9 | } from "./utils/constants"; 10 | 11 | export class PlayerTechFactory { 12 | constructor(wrapperId, manifestUrl) { 13 | this.wrapperId_ = wrapperId; 14 | this.manifestUrl_ = manifestUrl; 15 | } 16 | 17 | constructPlayerTech() { 18 | return new Promise((resolve, reject) => { 19 | this.determineType_(this.manifestUrl_) 20 | .then(type => { 21 | let player; 22 | if (type === ENUM_TYPE_HLS) { 23 | player = new HlsPlayer(this.wrapperId_, this.manifestUrl_); 24 | } else if (type === ENUM_TYPE_MPEGDASH) { 25 | player = new DashPlayer(this.wrapperId_, this.manifestUrl_); 26 | } else if (type === ENUM_TYPE_MSS) { 27 | player = new MssPlayer(this.wrapperId_, this.manifestUrl_); 28 | } else { 29 | reject("Internal error: no available player tech found!"); 30 | } 31 | player 32 | .load() 33 | .then(() => { 34 | resolve(player); 35 | }) 36 | .catch(reject); 37 | }) 38 | .catch(reject); 39 | }); 40 | } 41 | 42 | determineType_(uri) { 43 | return new Promise((resolve, reject) => { 44 | fetch(uri) 45 | .then(resp => { 46 | let type = CONTENT_TYPE_MAP[resp.headers["content-type"]]; 47 | if (!type) { 48 | if (uri.match(/\.m3u8/)) { 49 | type = ENUM_TYPE_HLS; 50 | } else if (uri.match(/\.mpd/)) { 51 | type = ENUM_TYPE_MPEGDASH; 52 | } else if (uri.match(/\/Manifest/)) { 53 | type = ENUM_TYPE_MSS; 54 | } else if (uri.match(/\/manifest/)) { 55 | type = ENUM_TYPE_MSS; 56 | } 57 | } 58 | resolve(type); 59 | }) 60 | .catch(err => { 61 | console.error(err); 62 | reject(err); 63 | }); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | const ENUM_TYPE_NO_CONTENT_TYPE = "BAD_CONTENT_TYPE"; 2 | 3 | export const CONTENT_TYPE_MAP = { 4 | "application/x-mpegURL": ENUM_TYPE_HLS, 5 | "application/octet-stream": ENUM_TYPE_NO_CONTENT_TYPE, 6 | "binary/octet-stream": ENUM_TYPE_NO_CONTENT_TYPE, 7 | "application/vnd.apple.mpegurl": ENUM_TYPE_HLS, 8 | "application/dash+xml": ENUM_TYPE_MPEGDASH, 9 | "application/vnd.apple.mpegurl;charset=UTF-8": ENUM_TYPE_HLS, 10 | "application/vnd.ms-sstr+xml": ENUM_TYPE_MSS 11 | }; 12 | 13 | export const ENUM_TYPE_HLS = "HLS"; 14 | export const ENUM_TYPE_MPEGDASH = "MPD"; 15 | export const ENUM_TYPE_MSS = "MSS"; 16 | 17 | export const DEFAULT_OPTIONS = { 18 | skin: true 19 | }; 20 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "skin.scss"; 2 | 3 | .eyevinn-player { 4 | width: 100%; 5 | height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /styles/skin.scss: -------------------------------------------------------------------------------- 1 | .player-controller { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | background-color: black; 8 | opacity: 0.5; 9 | transition: visibility 0s linear 0.7s, opacity 0.7s ease-in-out; 10 | 11 | position: absolute; 12 | bottom: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 15%; 16 | } 17 | 18 | .player-controller > * { 19 | margin: 0 5px; 20 | } 21 | 22 | .player-controller-visible { 23 | transition-delay: 0s;visibility: visible; opacity: 0.5; 24 | } 25 | 26 | .player-controller-hidden { 27 | opacity: 0; visibility:hidden; 28 | } 29 | 30 | .player-timeline-container { 31 | width: 50%; 32 | height: 10%; 33 | 34 | display: flex; 35 | flex-direction: row; 36 | align-items: center; 37 | } 38 | 39 | .player-timeline { 40 | border: solid 1px white; 41 | width: 85%; 42 | height: 4px; 43 | margin: 0 auto; 44 | } 45 | 46 | .player-timeline:hover { 47 | cursor: pointer; 48 | } 49 | 50 | .player-timeline-mark { 51 | width: 85%; 52 | margin: 0 auto; 53 | } 54 | 55 | .player-timeline-position { 56 | font-family: LevelOne; 57 | font-size: 0.8em; 58 | color: white; 59 | float: left; 60 | } 61 | 62 | .player-timeline-duration { 63 | font-family: LevelOne; 64 | font-size: 0.8em; 65 | color: white; 66 | float: right; 67 | } 68 | 69 | .player-btn-toggle-play { 70 | height: 50%; 71 | width: 5%; 72 | border-radius: 7pt; 73 | padding: 2px; 74 | } 75 | 76 | .player-btn-play { 77 | background: url('../icons/media-play.svg') center no-repeat; 78 | background-size: 50%; 79 | background-color: white; 80 | } 81 | 82 | .player-btn-pause { 83 | background: url('../icons/media-pause.svg') center no-repeat; 84 | background-size: 50%; 85 | background-color: white; 86 | } 87 | 88 | .player-btn-toggle-audio { 89 | height: 50%; 90 | width: 5%; 91 | border-radius: 7pt; 92 | padding: 2px; 93 | } 94 | 95 | .player-btn-audio-on { 96 | background: url('../icons/volume-high.svg') center no-repeat; 97 | background-size: 50%; 98 | background-color: white; 99 | } 100 | 101 | .player-btn-audio-off { 102 | background: url('../icons/volume-off.svg') center no-repeat; 103 | background-size: 50%; 104 | background-color: white; 105 | } 106 | 107 | .player-btn-hover { 108 | background-color: rgb(15,186,240); 109 | } 110 | 111 | .player-btn-big { 112 | position: absolute; 113 | top: 50%; 114 | left: 50%; 115 | transform: translate(-50%, -50%); 116 | width: 33%; 117 | height: 33%; 118 | margin: 2%; 119 | z-index: 20; 120 | opacity: 0.5; 121 | border-radius: 40px; 122 | background: url('../icons/media-play.svg') center no-repeat; 123 | background-size: 33%; 124 | background-color: white; 125 | cursor: pointer; 126 | } 127 | 128 | .player-btn-big-hidden { 129 | display: none; 130 | } 131 | 132 | .player-btn-big-visible { 133 | display: block; 134 | } 135 | 136 | .player-logo { 137 | background: url('../icons/eyevinnlogo.png') no-repeat; 138 | background-size: contain; 139 | height: 70%; 140 | width: 25%; 141 | } 142 | 143 | @media (max-width: 850px) { 144 | .player-timeline-container { 145 | display: none; 146 | } 147 | } 148 | --------------------------------------------------------------------------------