├── .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 | [](https://badge.fury.io/js/dash-shaka-playback)
2 | [](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 | [](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 |
--------------------------------------------------------------------------------