├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .yarnclean ├── LICENSE ├── README.md ├── bump ├── package.json ├── public ├── index.html └── screen-shot-dash-clappr.png ├── src └── clappr-dash-shaka-playback.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "modules": "commonjs" }]], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "never" 28 | ] 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | appveyor.yml 28 | circle.yml 29 | codeship-services.yml 30 | codeship-steps.yml 31 | wercker.yml 32 | .tern-project 33 | .gitattributes 34 | .editorconfig 35 | .*ignore 36 | .flowconfig 37 | .documentup.json 38 | .yarn-metadata.json 39 | .travis.yml 40 | 41 | # misc 42 | *.md 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Globo.com Player authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Globo.com nor the names of its contributors 12 | may be used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/dash-shaka-playback.svg)](https://badge.fury.io/js/dash-shaka-playback) 2 | [![license](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg) 3 | 4 | # dash-shaka-playback 5 | 6 | A [clappr](https://github.com/clappr/clappr) playback to play dash based on the amazing [shaka-player](https://github.com/google/shaka-player). 7 | 8 | > CDN JSDELIVR: https://cdn.jsdelivr.net/gh/clappr/dash-shaka-playback@latest/dist/dash-shaka-playback.js 9 | > 10 | > CDNJS: https://cdnjs.cloudflare.com/ajax/libs/dash-shaka-playback/2.0.5/dash-shaka-playback.js 11 | > 12 | > NPM: https://www.npmjs.com/package/dash-shaka-playback/ 13 | 14 | ## Changelog 15 | 16 | * supports closed caption (subtitles) 17 | 18 | # Demo 19 | 20 | [![dash shaka playback screenshot](https://raw.githubusercontent.com/clappr/dash-shaka-playback/master/public/screen-shot-dash-clappr.png)](https://jsfiddle.net/m8ndduLo/69/) 21 | 22 | # Usage 23 | 24 | ```html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 50 | 51 | 52 | ``` 53 | 54 | # DRM 55 | 56 | If need to protect your content (DRM) you must use the `shakaConfiguration` following the [shaka configuration](http://shaka-player-demo.appspot.com/docs/api/tutorial-drm-config.html) need. 57 | 58 | # License Wrapping 59 | 60 | If need to wrap DRM license requests or responses you use `shakaOnBeforeLoad` following [shaka License Wrapping](http://shaka-player-demo.appspot.com/docs/api/tutorial-license-wrapping.html) guide. 61 | 62 | # Development 63 | 64 | Install yarn: 65 | 66 | https://yarnpkg.com/lang/en/docs/install/ 67 | 68 | Install dependencies: 69 | 70 | `yarn install` 71 | 72 | Run dev. server : 73 | 74 | `yarn start` 75 | 76 | By default, dev. server is listening on `http://0.0.0.0:8080`. 77 | 78 | Build plugin: 79 | 80 | `yarn dist` 81 | 82 | By default, Shaka player is bundled with plugin. A "lightweight" version of this plugin, without shaka player bundled, `dash-shaka-playback-external.min.js` is available. 83 | 84 | # "extra" features 85 | 86 | This playback offers you an API for handling with: audio, video and text tracks. 87 | 88 | ```javascript 89 | selectTrack(track) 90 | textTracks() 91 | audioTracks() 92 | videoTracks() 93 | ``` 94 | 95 | # For the older versions [check](https://github.com/clappr/dash-shaka-playback/tree/releases) 96 | -------------------------------------------------------------------------------- /bump: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_NAME='dash-shaka-playback' 4 | CDN_PATH="gh/clappr/dash-shaka-playback@latest/dist/$PROJECT_NAME.min.js" 5 | 6 | update_dependencies() { 7 | echo 'updating dependencies' && 8 | yarn install 9 | } 10 | 11 | update_version() { 12 | current_tag=$(git describe --abbrev=0 --tags master) && 13 | echo 'bump from '$current_tag' to '$1 && 14 | sed -i ".bkp" "s/\(version\":[ ]*\"\)$current_tag/\1$1/" package.json 15 | } 16 | 17 | build() { 18 | echo "building $PROJECT_NAME.js" && 19 | yarn build && 20 | echo "building $PROJECT_NAME.min.js" && 21 | yarn release 22 | } 23 | 24 | run_tests() { 25 | yarn lint 26 | } 27 | 28 | make_release_commit() { 29 | git add package.json yarn.lock && 30 | git commit -m 'chore(package): bump version' && 31 | git tag -m "$1" $1 32 | } 33 | 34 | git_push() { 35 | echo 'pushing to github' 36 | git push origin master --tags 37 | } 38 | 39 | npm_publish() { 40 | npm publish 41 | } 42 | 43 | purge_cdn_cache() { 44 | echo 'purging cdn cache' 45 | curl -q "http://purge.jsdelivr.net/$CDN_PATH" 46 | } 47 | 48 | main() { 49 | npm whoami 50 | if (("$?" != "0")); then 51 | echo "you are not logged into npm" 52 | exit 1 53 | fi 54 | update_dependencies && 55 | update_version $1 && 56 | build 57 | if (("$?" != "0")); then 58 | echo "something failed during dependency update, version update, or build" 59 | exit 1 60 | fi 61 | run_tests 62 | if (("$?" == "0")); then 63 | make_release_commit $1 && 64 | git_push && 65 | npm_publish && 66 | purge_cdn_cache && 67 | exit 0 68 | 69 | echo "something failed" 70 | exit 1 71 | else 72 | echo "you broke the tests. fix it before bumping another version." 73 | exit 1 74 | fi 75 | } 76 | 77 | if [ "$1" != "" ]; then 78 | main $1 79 | else 80 | echo "Usage: bump [new_version]" 81 | fi 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash-shaka-playback", 3 | "version": "3.2.0", 4 | "description": "clappr dash playback based on shaka player", 5 | "main": "./dist/dash-shaka-playback.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:clappr/dash-shaka-playback.git" 9 | }, 10 | "scripts": { 11 | "build": "webpack", 12 | "dist": "yarn lint && yarn build && yarn release", 13 | "start": "webpack-dev-server", 14 | "release": "webpack", 15 | "lint": "eslint src/", 16 | "prepublishOnly": "yarn dist" 17 | }, 18 | "files": [ 19 | "/dist", 20 | "/src" 21 | ], 22 | "author": "Clappr team", 23 | "license": "BSD-3-Clause", 24 | "peerDependencies": { 25 | "@clappr/core": "^0.4.17" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.12.10", 29 | "@babel/preset-env": "^7.12.11", 30 | "babel-loader": "^8.2.2", 31 | "babel-plugin-add-module-exports": "^1.0.4", 32 | "eslint": "^7.18.0", 33 | "eslint-config-standard": "^16.0.2", 34 | "eslint-plugin-import": "^2.22.1", 35 | "eslint-plugin-node": "^11.1.0", 36 | "eslint-plugin-promise": "^4.2.1", 37 | "eslint-plugin-standard": "^4.1.0", 38 | "shaka-player": "^3.0.7", 39 | "uglifyjs-webpack-plugin": "^2.2.0", 40 | "webpack": "^4.46.0", 41 | "webpack-cli": "^3.3.12", 42 | "webpack-dev-server": "^3.11.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | clappr dash shaka 6 | 7 | 8 |
9 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/screen-shot-dash-clappr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/dash-shaka-playback/0270bda07dcce270594497d7d2486144d65c9687/public/screen-shot-dash-clappr.png -------------------------------------------------------------------------------- /src/clappr-dash-shaka-playback.js: -------------------------------------------------------------------------------- 1 | import {HTML5Video, Log, Events, PlayerError} from 'clappr' 2 | import shaka from 'shaka-player' 3 | 4 | const SEND_STATS_INTERVAL_MS = 30 * 1e3 5 | const DEFAULT_LEVEL_AUTO = -1 6 | 7 | class DashShakaPlayback extends HTML5Video { 8 | static get Events () { 9 | return { 10 | SHAKA_READY: 'shaka:ready' 11 | } 12 | } 13 | 14 | static get shakaPlayer() { return shaka } 15 | 16 | static canPlay (resource, mimeType = '') { 17 | shaka.polyfill.installAll() 18 | let browserSupported = shaka.Player.isBrowserSupported() 19 | let resourceParts = resource.split('?')[0].match(/.*\.(.*)$/) || [] 20 | return browserSupported && ((resourceParts[1] === 'mpd') || mimeType.indexOf('application/dash+xml') > -1) 21 | } 22 | 23 | get name () { 24 | return 'dash_shaka_playback' 25 | } 26 | 27 | get shakaVersion () { 28 | return shaka.player.Player.version 29 | } 30 | 31 | get shakaPlayerInstance () { 32 | return this._player 33 | } 34 | 35 | get levels () { 36 | return this._levels 37 | } 38 | 39 | get seekRange() { 40 | if (!this.shakaPlayerInstance) return { start: 0, end: 0} 41 | 42 | return this.shakaPlayerInstance.seekRange() 43 | } 44 | 45 | set currentLevel (id) { 46 | this._currentLevelId = id 47 | let isAuto = this._currentLevelId === DEFAULT_LEVEL_AUTO 48 | 49 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH_START) 50 | if (!isAuto) { 51 | this._player.configure({abr: {enabled: false}}) 52 | this._pendingAdaptationEvent = true 53 | this.selectTrack(this.videoTracks.filter((t) => t.id === this._currentLevelId)[0]) 54 | } 55 | else { 56 | this._player.configure({abr: {enabled: true}}) 57 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH_END) 58 | } 59 | } 60 | 61 | get currentLevel () { 62 | return this._currentLevelId || DEFAULT_LEVEL_AUTO 63 | } 64 | 65 | get dvrEnabled() { 66 | return this._duration >= this._minDvrSize && this.getPlaybackType() === 'live' 67 | } 68 | 69 | get latency() { 70 | if (!this.shakaPlayerInstance) return 0 71 | return this.shakaPlayerInstance.getStats().liveLatency 72 | } 73 | 74 | get currentProgramDateTime() { 75 | if (!this.shakaPlayerInstance) return null 76 | return this.shakaPlayerInstance.getPlayheadTimeAsDate() 77 | } 78 | 79 | getDuration() { 80 | return this._duration 81 | } 82 | 83 | get _duration() { 84 | if (!this.shakaPlayerInstance) return 0 85 | 86 | return this.seekRange.end - this.seekRange.start 87 | } 88 | 89 | getCurrentTime() { 90 | if (!this.shakaPlayerInstance) return 0 91 | const shakaMediaElement = this.shakaPlayerInstance.getMediaElement() 92 | return shakaMediaElement ? shakaMediaElement.currentTime - this.seekRange.start : 0 93 | } 94 | 95 | get _startTime() { 96 | return this.seekRange.start 97 | } 98 | 99 | get presentationStartTimeAsDate() { 100 | if (!this.shakaPlayerInstance || !this.shakaPlayerInstance.getPresentationStartTimeAsDate()) return 0 101 | 102 | return new Date(this.shakaPlayerInstance.getPresentationStartTimeAsDate().getTime() + this.seekRange.start * 1000) 103 | } 104 | 105 | get bandwidthEstimate() { 106 | if (!this.shakaPlayerInstance) return null 107 | return this.shakaPlayerInstance.getStats().estimatedBandwidth 108 | } 109 | 110 | get sourceMedia() { 111 | return this._options.src 112 | } 113 | 114 | constructor (...args) { 115 | super(...args) 116 | this._levels = [] 117 | this._pendingAdaptationEvent = false 118 | this._isShakaReadyState = false 119 | 120 | this._minDvrSize = typeof (this.options.shakaMinimumDvrSize) === 'undefined' ? 60 : this.options.shakaMinimumDvrSize 121 | } 122 | 123 | getProgramDateTime() { 124 | return this.presentationStartTimeAsDate 125 | } 126 | 127 | _updateDvr(status) { 128 | this.trigger(Events.PLAYBACK_DVR, status) 129 | this.trigger(Events.PLAYBACK_STATS_ADD, { 'dvr': status }) 130 | } 131 | 132 | seek(time) { 133 | if (time < 0) { 134 | Log.warn('Attempt to seek to a negative time. Resetting to live point. Use seekToLivePoint() to seek to the live point.') 135 | time = this._duration 136 | } 137 | // assume live if time within 3 seconds of end of stream 138 | this.dvrEnabled && this._updateDvr(time < this._duration-3) 139 | time += this._startTime 140 | this.el.currentTime = time 141 | } 142 | 143 | pause() { 144 | this.el.pause() 145 | this.dvrEnabled && this._updateDvr(true) 146 | } 147 | 148 | play () { 149 | if (!this._player) this.load() 150 | if (!this.isReady) { 151 | this.once(DashShakaPlayback.Events.SHAKA_READY, this.play) 152 | return 153 | } 154 | super.play() 155 | this._startTimeUpdateTimer() 156 | this._stopped = false 157 | this._src = this.el.src 158 | } 159 | 160 | load(source) { 161 | if (source) this._options.src = source 162 | this._setup() 163 | } 164 | 165 | _onPlaying() { 166 | /* 167 | The `_onPlaying` should not be called while buffering: https://github.com/google/shaka-player/issues/2230 168 | It will be executed on bufferfull. 169 | */ 170 | if (this._isBuffering) return 171 | return super._onPlaying() 172 | } 173 | 174 | _onSeeking() { 175 | this._isSeeking = true 176 | return super._onSeeking() 177 | } 178 | 179 | _onSeeked() { 180 | /* 181 | The `_onSeeked` should not be called while buffering. 182 | It will be executed on bufferfull. 183 | */ 184 | if (this._isBuffering) return 185 | 186 | this._isSeeking = false 187 | return super._onSeeked() 188 | } 189 | 190 | _startTimeUpdateTimer() { 191 | this._stopTimeUpdateTimer() 192 | this._timeUpdateTimer = setInterval(() => { 193 | this._onTimeUpdate() 194 | }, 100) 195 | } 196 | 197 | _stopTimeUpdateTimer() { 198 | this._timeUpdateTimer && clearInterval(this._timeUpdateTimer) 199 | } 200 | 201 | // skipping HTML5Video `_setupSrc` (on tag video) 202 | _setupSrc () {} 203 | 204 | // skipping ready event on video tag in favor of ready on shaka 205 | _ready () { 206 | // override with no-op 207 | } 208 | 209 | _onShakaReady() { 210 | this._isShakaReadyState = true 211 | this.trigger(DashShakaPlayback.Events.SHAKA_READY) 212 | this.trigger(Events.PLAYBACK_READY, this.name) 213 | } 214 | 215 | get isReady () { 216 | return this._isShakaReadyState 217 | } 218 | 219 | // skipping error handling on video tag in favor of error on shaka 220 | error (event) { 221 | Log.error('an error was raised by the video tag', event, this.el.error) 222 | } 223 | 224 | isHighDefinitionInUse () { 225 | return !!this.highDefinition 226 | } 227 | 228 | stop () { 229 | this._stopTimeUpdateTimer() 230 | clearInterval(this.sendStatsId) 231 | this._stopped = true 232 | 233 | if (this._player) { 234 | this._sendStats() 235 | 236 | this._player.unload().then(() => { 237 | super.stop() 238 | this._player = null 239 | this._isShakaReadyState = false 240 | }).catch(() => { 241 | Log.error('shaka could not be unloaded') 242 | }) 243 | } else { 244 | super.stop() 245 | } 246 | } 247 | 248 | get textTracks () { 249 | return this.isReady && this._player.getTextTracks() 250 | } 251 | 252 | get audioTracks () { 253 | return this.isReady && this._player.getVariantTracks().filter((t) => t.mimeType.startsWith('audio/')) 254 | } 255 | 256 | get videoTracks () { 257 | return this.isReady && this._player.getVariantTracks().filter((t) => t.mimeType.startsWith('video/')) 258 | } 259 | 260 | getPlaybackType () { 261 | return (this.isReady && this._player.isLive() ? 'live' : 'vod') || '' 262 | } 263 | 264 | selectTrack (track) { 265 | if (track.type === 'text') { 266 | this._player.selectTextTrack(track) 267 | } else if (track.type === 'variant') { 268 | this._player.selectVariantTrack(track) 269 | if (track.mimeType.startsWith('video/')) { 270 | // we trigger the adaptation event here 271 | // because Shaka doesn't trigger its event on "manual" selection. 272 | this._onAdaptation() 273 | } 274 | } else { 275 | throw new Error('Unhandled track type:', track.type) 276 | } 277 | } 278 | 279 | /** 280 | * @override 281 | */ 282 | get closedCaptionsTracks() { 283 | let id = 0 284 | let trackId = () => { return id++ } 285 | let tracks = this.textTracks || [] 286 | 287 | return tracks 288 | .filter(track => track.kind === 'subtitle') 289 | .map(track => { return {id: trackId(), name: track.label || track.language, track: track} }) 290 | } 291 | 292 | /** 293 | * @override 294 | */ 295 | get closedCaptionsTrackId() { 296 | return super.closedCaptionsTrackId 297 | } 298 | 299 | /** 300 | * @override 301 | */ 302 | set closedCaptionsTrackId(trackId) { 303 | if (!this._player) { 304 | return 305 | } 306 | 307 | let tracks = this.closedCaptionsTracks 308 | let showingTrack 309 | 310 | // Note: -1 is for hide all tracks 311 | if (trackId !== -1) { 312 | showingTrack = tracks.find(track => track.id === trackId) 313 | if (!showingTrack) { 314 | Log.warn(`Track id "${trackId}" not found`) 315 | return 316 | } 317 | if (this._shakaTTVisible && showingTrack.track.active === true) { 318 | Log.info(`Track id "${trackId}" already showing`) 319 | return 320 | } 321 | } 322 | 323 | if (showingTrack) { 324 | this._player.selectTextTrack(showingTrack.track) 325 | this._player.setTextTrackVisibility(true) 326 | this._enableShakaTextTrack(true) 327 | } else { 328 | this._player.setTextTrackVisibility(false) 329 | this._enableShakaTextTrack(false) 330 | } 331 | 332 | this._ccTrackId = trackId 333 | this.trigger(Events.PLAYBACK_SUBTITLE_CHANGED, { 334 | id: trackId 335 | }) 336 | } 337 | 338 | _enableShakaTextTrack(isEnable) { 339 | // Shaka player use only one TextTrack object with video element to handle all text tracks 340 | // It must be enabled or disabled in addition to call selectTextTrack() 341 | if (!this.el.textTracks) { 342 | return 343 | } 344 | 345 | this._shakaTTVisible = isEnable 346 | 347 | Array.from(this.el.textTracks) 348 | .filter(track => track.kind === 'subtitles') 349 | .forEach(track => track.mode = isEnable === true ? 'showing' : 'hidden') 350 | } 351 | 352 | _checkForClosedCaptions() { 353 | if (this._ccIsSetup) { 354 | return 355 | } 356 | 357 | if (this.hasClosedCaptionsTracks) { 358 | this.trigger(Events.PLAYBACK_SUBTITLE_AVAILABLE) 359 | const trackId = this.closedCaptionsTrackId 360 | this.closedCaptionsTrackId = trackId 361 | } 362 | this._ccIsSetup = true 363 | } 364 | 365 | destroy () { 366 | this._stopTimeUpdateTimer() 367 | clearInterval(this.sendStatsId) 368 | 369 | if (this._player) { 370 | this._player.destroy() 371 | .then(() => this._destroy()) 372 | .catch(() => { 373 | this._destroy() 374 | Log.error('shaka could not be destroyed') 375 | }) 376 | } else { 377 | this._destroy() 378 | } 379 | 380 | super.destroy() 381 | } 382 | 383 | _setup() { 384 | this._isShakaReadyState = false 385 | this._ccIsSetup = false 386 | 387 | let runAllSteps = () => { 388 | this._player = this._createPlayer() 389 | this._setInitialConfig() 390 | this._loadSource() 391 | } 392 | 393 | this._player 394 | ? this._player.destroy().then(() => runAllSteps()) 395 | : runAllSteps() 396 | } 397 | 398 | _createPlayer() { 399 | let player = new shaka.Player(this.el) 400 | player.addEventListener('error', this._onError.bind(this)) 401 | player.addEventListener('adaptation', this._onAdaptation.bind(this)) 402 | player.addEventListener('buffering', this._handleShakaBufferingEvents.bind(this)) 403 | return player 404 | } 405 | 406 | _setInitialConfig() { 407 | this._options.shakaConfiguration && this._player.configure(this._options.shakaConfiguration) 408 | this._options.shakaOnBeforeLoad && this._options.shakaOnBeforeLoad(this._player) 409 | } 410 | 411 | _loadSource() { 412 | this._player.load(this._options.src) 413 | .then(() => this._loaded()) 414 | .catch((e) => this._setupError(e)) 415 | } 416 | 417 | _onTimeUpdate() { 418 | if (!this.shakaPlayerInstance) return 419 | 420 | let update = { 421 | current: this.getCurrentTime(), 422 | total: this.getDuration(), 423 | firstFragDateTime: this.getProgramDateTime() 424 | } 425 | let isSame = this._lastTimeUpdate && ( 426 | update.current === this._lastTimeUpdate.current && 427 | update.total === this._lastTimeUpdate.total) 428 | if (isSame) 429 | return 430 | 431 | this._lastTimeUpdate = update 432 | this.trigger(Events.PLAYBACK_TIMEUPDATE, update, this.name) 433 | } 434 | 435 | // skipping HTML5 `_handleBufferingEvents` in favor of shaka buffering events 436 | _handleBufferingEvents() {} 437 | 438 | _handleShakaBufferingEvents(e) { 439 | if (this._stopped) return 440 | 441 | this._isBuffering = e.buffering 442 | this._isBuffering ? this._onBuffering() : this._onBufferfull() 443 | } 444 | 445 | _onBuffering () { 446 | this.trigger(Events.PLAYBACK_BUFFERING) 447 | } 448 | 449 | _onBufferfull() { 450 | this.trigger(Events.PLAYBACK_BUFFERFULL) 451 | if (this._isSeeking) this._onSeeked() 452 | if (this.isPlaying()) this._onPlaying() 453 | } 454 | 455 | _loaded () { 456 | this._onShakaReady() 457 | this._startToSendStats() 458 | this._fillLevels() 459 | this._checkForClosedCaptions() 460 | } 461 | 462 | _fillLevels () { 463 | if (this._levels.length === 0) { 464 | this._levels = this.videoTracks.map((videoTrack) => { return {id: videoTrack.id, label: `${videoTrack.height}p`} }).reverse() 465 | this.trigger(Events.PLAYBACK_LEVELS_AVAILABLE, this.levels) 466 | } 467 | } 468 | 469 | _startToSendStats () { 470 | const intervalMs = this._options.shakaSendStatsInterval || SEND_STATS_INTERVAL_MS 471 | this.sendStatsId = setInterval(() => this._sendStats(), intervalMs) 472 | } 473 | 474 | _sendStats () { 475 | this.trigger(Events.PLAYBACK_STATS_ADD, this._player.getStats()) 476 | } 477 | 478 | _setupError (err) { 479 | this._onError(err) 480 | } 481 | 482 | _onError (err) { 483 | const error = { 484 | shakaError: err, 485 | videoError: this.el.error 486 | } 487 | 488 | let { category, code, severity } = error.shakaError.detail || error.shakaError 489 | 490 | if (error.videoError || !code && !category) return super._onError() 491 | 492 | const isCritical = severity === shaka.util.Error.Severity.CRITICAL 493 | const errorData = { 494 | code: `${category}_${code}`, 495 | description: `Category: ${category}, code: ${code}, severity: ${severity}`, 496 | level: isCritical ? PlayerError.Levels.FATAL : PlayerError.Levels.WARN, 497 | raw: err 498 | } 499 | const formattedError = this.createError(errorData) 500 | Log.error('Shaka error event:', formattedError) 501 | this.trigger(Events.PLAYBACK_ERROR, formattedError) 502 | } 503 | 504 | 505 | _onAdaptation () { 506 | let activeVideo = this.videoTracks.filter((t) => t.active === true)[0] 507 | 508 | this._fillLevels() 509 | 510 | // update stats that may have changed before we trigger event 511 | // so that user can rely on stats data when handling event 512 | this._sendStats() 513 | 514 | if (this._pendingAdaptationEvent) { 515 | this.trigger(Events.PLAYBACK_LEVEL_SWITCH_END) 516 | this._pendingAdaptationEvent = false 517 | } 518 | 519 | Log.debug('an adaptation has happened:', activeVideo) 520 | this.highDefinition = (activeVideo.height >= 720) 521 | this.trigger(Events.PLAYBACK_HIGHDEFINITIONUPDATE, this.highDefinition) 522 | this.trigger(Events.PLAYBACK_BITRATE, { 523 | bandwidth: activeVideo.bandwidth, 524 | width: activeVideo.width, 525 | height: activeVideo.height, 526 | level: activeVideo.id, 527 | bitrate: activeVideo.videoBandwidth 528 | }) 529 | } 530 | 531 | _updateSettings() { 532 | if (this.getPlaybackType() === 'vod') 533 | this.settings.left = ['playpause', 'position', 'duration'] 534 | else if (this.dvrEnabled) 535 | this.settings.left = ['playpause'] 536 | else 537 | this.settings.left = ['playstop'] 538 | 539 | this.settings.seekEnabled = this.isSeekEnabled() 540 | this.trigger(Events.PLAYBACK_SETTINGSUPDATE) 541 | } 542 | 543 | _destroy () { 544 | this._isShakaReadyState = false 545 | Log.debug('shaka was destroyed') 546 | } 547 | } 548 | 549 | export default DashShakaPlayback 550 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | 4 | var NPM_RUN = process.env.npm_lifecycle_event 5 | 6 | const externals = () => { 7 | // By default, only Clappr is defined as external library 8 | return { 9 | clappr: { 10 | amd: 'clappr', 11 | commonjs: 'clappr', 12 | commonjs2: 'clappr', 13 | root: 'Clappr' 14 | } 15 | } 16 | } 17 | 18 | const webpackConfig = (config) => { 19 | return { 20 | devServer: { 21 | contentBase: [ 22 | path.resolve(__dirname, 'public'), 23 | ], 24 | disableHostCheck: true, // https://github.com/webpack/webpack-dev-server/issues/882 25 | compress: true, 26 | host: '0.0.0.0', 27 | port: 8181 28 | }, 29 | mode: config.mode, 30 | devtool: 'source-maps', 31 | entry: path.resolve(__dirname, 'src/clappr-dash-shaka-playback.js'), 32 | externals: config.externals, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.js$/, 37 | loader: 'babel-loader', 38 | include: [ 39 | path.resolve(__dirname, 'src') 40 | ] 41 | }, 42 | ], 43 | }, 44 | output: { 45 | path: path.resolve(__dirname, 'dist'), 46 | publicPath: 'dist/', 47 | filename: config.filename, 48 | library: 'DashShakaPlayback', 49 | libraryTarget: 'umd', 50 | }, 51 | plugins: config.plugins, 52 | } 53 | } 54 | 55 | var configurations = [] 56 | 57 | if (NPM_RUN === 'build' || NPM_RUN === 'start') { 58 | // Unminified bundle with shaka-player 59 | configurations.push(webpackConfig({ 60 | filename: 'dash-shaka-playback.js', 61 | plugins: [], 62 | externals: externals(), 63 | mode: 'development' 64 | })) 65 | 66 | // Unminified bundle without shaka-player 67 | var customExt = externals() 68 | customExt['shaka-player'] = 'shaka' 69 | configurations.push(webpackConfig({ 70 | filename: 'dash-shaka-playback.external.js', 71 | plugins: [], 72 | externals: customExt, 73 | mode: 'development' 74 | })) 75 | } 76 | 77 | if (NPM_RUN === 'release') { 78 | // Minified bundle with shaka-player 79 | configurations.push(webpackConfig({ 80 | filename: 'dash-shaka-playback.min.js', 81 | optimization: { 82 | minimizer: [ 83 | new UglifyJsPlugin({ 84 | sourceMap: true 85 | }), 86 | ] 87 | }, 88 | externals: externals(), 89 | mode: 'production' 90 | })) 91 | 92 | // Minified bundle without shaka-player 93 | var customExt = externals() 94 | customExt['shaka-player'] = 'shaka' 95 | configurations.push(webpackConfig({ 96 | filename: 'dash-shaka-playback.external.min.js', 97 | optimization: { 98 | minimizer: [ 99 | new UglifyJsPlugin({ 100 | sourceMap: true 101 | }), 102 | ] 103 | }, 104 | externals: customExt, 105 | mode: 'production' 106 | })) 107 | } 108 | 109 | // https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations 110 | module.exports = configurations 111 | --------------------------------------------------------------------------------