├── .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 | 
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 |
--------------------------------------------------------------------------------
/icons/media-play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/volume-high.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/volume-off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------