@mindedge/vuetify-player 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ <template>
2
+ <v-card>
3
+ <v-card-title>{{ t(language, 'playlist.up_next') }}</v-card-title>
4
+ <v-card-text>
5
+ <v-list>
6
+ <v-list-item-group v-model="sourceIndex">
7
+ <v-list-item
8
+ v-for="(source, index) of playlist"
9
+ :key="index + 'playlistSources'"
10
+ @click="onPlaylistSelect(index)"
11
+ >
12
+ <v-list-item-icon>
13
+ <v-avatar
14
+ v-if="getPoster(source.poster, poster)"
15
+ tile
16
+ >
17
+ <img :src="getPoster(source.poster, poster)" />
18
+ </v-avatar>
19
+ <v-skeleton-loader
20
+ v-if="!getPoster(source.poster, poster)"
21
+ class="ma-3"
22
+ type="avatar"
23
+ tile
24
+ ></v-skeleton-loader>
25
+ </v-list-item-icon>
26
+ <v-list-item-content>
27
+ <v-tooltip bottom>
28
+ <template v-slot:activator="{ on, attrs }">
29
+ <div
30
+ v-bind="attrs"
31
+ v-on="on"
32
+ class="text-lg-subtitle-1 text-truncate"
33
+ >
34
+ {{
35
+ source.name ||
36
+ t(language, 'playlist.default_name')
37
+ }}
38
+ </div>
39
+ </template>
40
+ <span>
41
+ {{
42
+ source.name ||
43
+ t(language, 'playlist.default_name')
44
+ }}
45
+ </span>
46
+ </v-tooltip>
47
+ </v-list-item-content>
48
+ </v-list-item>
49
+ </v-list-item-group>
50
+ </v-list>
51
+ </v-card-text>
52
+ <v-card-actions>
53
+ <v-col cols="6">
54
+ <v-btn
55
+ block
56
+ :disabled="playlist.length < 1 || sourceIndex === 0"
57
+ @click="onPlaylistSelect(sourceIndex - 1)"
58
+ >
59
+ <v-icon>mdi-skip-previous</v-icon>
60
+ <span class="d-sr-only">{{
61
+ t(language, 'playlist.previous')
62
+ }}</span>
63
+ </v-btn>
64
+ </v-col>
65
+ <v-col cols="6">
66
+ <v-btn
67
+ block
68
+ :disabled="
69
+ playlist.length < 1 ||
70
+ sourceIndex === playlist.length - 1
71
+ "
72
+ @click="onPlaylistSelect(sourceIndex + 1)"
73
+ >
74
+ <v-icon>mdi-skip-next</v-icon>
75
+ <span class="d-sr-only">{{
76
+ t(language, 'playlist.next')
77
+ }}</span>
78
+ </v-btn>
79
+ </v-col>
80
+ </v-card-actions>
81
+ </v-card>
82
+ </template>
83
+
84
+ <script>
85
+ import { t } from '../../i18n/i18n'
86
+
87
+ export default {
88
+ props: {
89
+ value: { type: Number, required: true },
90
+ playlist: { type: Array, required: true },
91
+ poster: { type: String, required: false, default: '' },
92
+ language: { type: String, required: false, default: 'en-US' },
93
+ },
94
+ data() {
95
+ return {
96
+ t,
97
+ sourceIndex: this.value,
98
+ }
99
+ },
100
+ watch: {
101
+ value(newIndex) {
102
+ this.sourceIndex = newIndex
103
+ },
104
+ },
105
+ methods: {
106
+ onPlaylistSelect(index) {
107
+ this.sourceIndex = index
108
+ this.$emit('input', parseInt(this.sourceIndex))
109
+ this.$emit('click:select', index)
110
+ },
111
+ getPoster(...posters) {
112
+ for (const poster of posters) {
113
+ if (poster) {
114
+ return poster
115
+ }
116
+ }
117
+ return null
118
+ },
119
+ },
120
+ mounted() {},
121
+ }
122
+ </script>
123
+
124
+ <style scoped>
125
+ .captions-list {
126
+ max-height: 10em;
127
+ overflow-y: scroll;
128
+ /* Fade the top/bottom 20% effect. The "red" mask is so the scrollbar doesn't get this effect*/
129
+ mask: linear-gradient(90deg, rgba(255, 0, 0, 0) 98%, rgba(255, 0, 0, 1) 98%),
130
+ linear-gradient(
131
+ 0deg,
132
+ rgba(0, 0, 0, 0) 0%,
133
+ rgba(0, 0, 0, 1) 20%,
134
+ rgba(0, 0, 0, 1) 80%,
135
+ rgba(0, 0, 0, 0) 100%
136
+ );
137
+ }
138
+ </style>
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <v-container>
3
+ <v-responsive :aspect-ratio="16 / 9">
4
+ <v-skeleton-loader
5
+ v-if="!player.ready"
6
+ class="mx-auto player-skeleton"
7
+ type="image"
8
+ ></v-skeleton-loader>
9
+
10
+ <div :id="player.id" :class="playerClass"></div>
11
+ </v-responsive>
12
+ </v-container>
13
+ </template>
14
+
15
+ <script>
16
+ export default {
17
+ name: 'YoutubePlayer',
18
+ props: {
19
+ language: { type: String, required: false, default: 'en-US' },
20
+ type: {
21
+ type: String,
22
+ required: false,
23
+ default: 'video',
24
+ },
25
+ attributes: {
26
+ type: Object,
27
+ required: true,
28
+ },
29
+ src: {
30
+ type: Object,
31
+ required: true,
32
+ },
33
+ },
34
+ watch: {},
35
+ computed: {
36
+ playerClass() {
37
+ let classList = 'player-' + this.type
38
+ return classList
39
+ },
40
+ },
41
+ data() {
42
+ return {
43
+ player: {
44
+ id:
45
+ 'yt-player-' +
46
+ Math.floor(Math.random() * 10000000) +
47
+ 1000000,
48
+ yt: {},
49
+ tag: {},
50
+ scriptTag: {},
51
+ loaded: false,
52
+ done: false,
53
+ ready: false,
54
+ },
55
+ }
56
+ },
57
+ methods: {
58
+ parseVideoSource(src) {
59
+ const result = {
60
+ videoId: null,
61
+ listId: null,
62
+ }
63
+
64
+ if (!src.sources || !src.sources.length || !src.sources[0].src) {
65
+ return result
66
+ } else {
67
+ let url = src.sources[0].src
68
+ const regexId =
69
+ /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
70
+ const idMatch = url.match(regexId)
71
+
72
+ if (idMatch && idMatch[2].length === 11) {
73
+ result.videoId = idMatch[2]
74
+ }
75
+
76
+ const regexPlaylist = /[?&]list=([^#&?]+)/
77
+ const playlistMatch = url.match(regexPlaylist)
78
+
79
+ if (playlistMatch && playlistMatch[1]) {
80
+ result.listId = playlistMatch[1]
81
+ }
82
+
83
+ return result
84
+ }
85
+ },
86
+ onPlayerReady() {
87
+ // Uncomment for auto-play
88
+ // e.target.playVideo();
89
+ this.player.ready = true
90
+
91
+ const source = this.parseVideoSource(this.src)
92
+
93
+ if (source.listId) {
94
+ this.ytPlayer.loadPlaylist(source.listId)
95
+ }
96
+ },
97
+ onPlayerStateChange(e) {
98
+ if (e.data == window.YT.PlayerState.PLAYING && !this.player.done) {
99
+ setTimeout(() => {
100
+ this.player.yt.stopVideo()
101
+ }, 6000)
102
+ this.player.done = true
103
+ }
104
+ },
105
+ tagReady() {
106
+ const source = this.parseVideoSource(this.src)
107
+
108
+ this.player.yt = new window.YT.Player(this.player.id, {
109
+ width: '100%',
110
+ videoId: source.videoId,
111
+ playerVars: {
112
+ playsinline: 1,
113
+ },
114
+ events: {
115
+ onReady: this.onPlayerReady,
116
+ onStateChange: this.onPlayerStateChange,
117
+ },
118
+ })
119
+ },
120
+ onreadystatechange() {
121
+ if (
122
+ !this.player.loaded &&
123
+ (this.player.tag.readyState === 'complete' ||
124
+ this.player.tag.readyState === 'loaded')
125
+ ) {
126
+ this.player.loaded = true
127
+ setTimeout(this.tagReady, 500)
128
+ }
129
+ },
130
+ onload() {
131
+ if (!this.player.loaded) {
132
+ this.player.loaded = true
133
+ setTimeout(this.tagReady, 500)
134
+ }
135
+ },
136
+ init() {
137
+ if (this.player.loaded) {
138
+ this.tagReady()
139
+ } else {
140
+ this.player.tag = document.createElement('script')
141
+
142
+ this.player.tag.src = 'https://www.youtube.com/iframe_api'
143
+ this.player.scriptTag =
144
+ document.getElementsByTagName('script')[0]
145
+
146
+ // Make sure script tag was successfully created
147
+ if (this.player.scriptTag) {
148
+ this.player.scriptTag.parentNode.insertBefore(
149
+ this.player.tag,
150
+ this.player.scriptTag
151
+ )
152
+
153
+ this.player.done = false
154
+ this.player.tag.onload = this.onload
155
+ this.player.tag.onreadystatechange = this.onreadystatechange
156
+ }
157
+ }
158
+ },
159
+ },
160
+ mounted() {
161
+ this.init()
162
+ },
163
+ }
164
+ </script>
165
+
166
+ <style scoped>
167
+ .player-skeleton {
168
+ position: relative;
169
+ margin-bottom: -400px;
170
+ height: 400px;
171
+ }
172
+ </style>
@@ -0,0 +1,270 @@
1
+ <template>
2
+ <div>
3
+ <v-row>
4
+ <v-col :cols="!playlistmenu || playlist.length <= 1 ? 12 : 8">
5
+ <YoutubePlayer
6
+ ref="youtubePlayer"
7
+ v-if="parseSourceType(current.src.sources) === 'youtube'"
8
+ :language="language"
9
+ :type="current.type"
10
+ :attributes="current.attributes"
11
+ :src="current.src"
12
+ @click:fullscreen="onFullscreen"
13
+ ></YoutubePlayer>
14
+ <Html5Player
15
+ ref="html5Player"
16
+ v-if="parseSourceType(current.src.sources) === 'html5'"
17
+ :language="language"
18
+ :type="current.type"
19
+ :attributes="current.attributes"
20
+ :src="current.src"
21
+ @load="$emit('load', $event)"
22
+ @ended="onEnded"
23
+ @loadeddata="onLoadeddata"
24
+ @loadedmetadata="$emit('loadedmetadata', $event)"
25
+ @play="$emit('play', $event)"
26
+ @pause="$emit('pause', $event)"
27
+ @seeking="$emit('seeking', $event)"
28
+ @timeupdate="$emit('timeupdate', $event)"
29
+ @progress="$emit('progress', $event)"
30
+ @canplay="$emit('canplay', $event)"
31
+ @waiting="$emit('waiting', $event)"
32
+ @canplaythrough="$emit('canplaythrough', $event)"
33
+ @error="$emit('error', $event)"
34
+ @emptied="$emit('emptied', $event)"
35
+ @ratechange="$emit('ratechange', $event)"
36
+ @stalled="$emit('stalled', $event)"
37
+ @abort="$emit('abort', $event)"
38
+ @mouseover="$emit('mouseover', $event)"
39
+ @mouseout="$emit('mouseout', $event)"
40
+ @click:fullscreen="onFullscreen"
41
+ @click:pictureinpicture="onPictureInPicture"
42
+ @click:remoteplayback="onRemoteplayback"
43
+ ></Html5Player>
44
+ </v-col>
45
+
46
+ <!-- Playlist stuff -->
47
+ <v-col v-if="playlistmenu && playlist.length > 1" cols="4">
48
+ <PlaylistMenu
49
+ v-model="sourceIndex"
50
+ :language="language"
51
+ :playlist="playlist"
52
+ :poster="poster"
53
+ @click:select="onPlaylistSelect"
54
+ ></PlaylistMenu>
55
+ </v-col>
56
+ </v-row>
57
+ </div>
58
+ </template>
59
+
60
+ <script>
61
+ import YoutubePlayer from './Media/YoutubePlayer.vue'
62
+ import Html5Player from './Media/Html5Player.vue'
63
+ import PlaylistMenu from './Media/PlaylistMenu.vue'
64
+
65
+ export default {
66
+ name: 'VuetifyPlayer',
67
+ components: {
68
+ YoutubePlayer,
69
+ Html5Player,
70
+ PlaylistMenu,
71
+ },
72
+ props: {
73
+ language: { type: String, required: false, default: 'en-US' },
74
+ src: {
75
+ type: Object,
76
+ required: false,
77
+ default: () => {
78
+ return {}
79
+ },
80
+ },
81
+ playlist: {
82
+ type: Array,
83
+ required: false,
84
+ default: () => {
85
+ return []
86
+ },
87
+ },
88
+ type: { type: String, required: false, default: 'video' }, // Allowed video|audio. In audio mode the player has a max-height of 40px
89
+ autoplay: { type: Boolean, required: false, default: false }, // Autoplay on load. It's in the spec but DON'T USE THIS
90
+ autopictureinpicture: {
91
+ type: Boolean,
92
+ required: false,
93
+ default: false,
94
+ }, // Start with picture in picture mode
95
+ controls: { type: Boolean, required: false, default: true }, // Show video controls. When false only play/pause allowed but clicking on the video itself
96
+ controlslist: {
97
+ type: String,
98
+ required: false,
99
+ default: 'nodownload noremoteplayback',
100
+ }, // Space separated string per <video>. Allowed 'nodownload nofullscreen noremoteplayback'
101
+ crossorigin: { type: String, required: false, default: 'anonymous' },
102
+ disablepictureinpicture: {
103
+ type: Boolean,
104
+ required: false,
105
+ default: true,
106
+ }, // Shows the picture in picture button
107
+ disableremoteplayback: {
108
+ type: Boolean,
109
+ required: false,
110
+ default: true,
111
+ }, // Shows the remote playback button but functionality does not exist when clicked
112
+ height: { type: String, required: false, default: 'auto' },
113
+ width: { type: String, required: false, default: '100%' },
114
+ rewind: { type: Boolean, required: false, default: false }, // Enabled the rewind 10s button
115
+ loop: { type: Boolean, required: false, default: false }, // Loop the video on completion
116
+ muted: { type: Boolean, required: false, default: false }, // Start the video muted
117
+ playsinline: { Boolean: String, required: false, default: false }, // Force inline & disable fullscreen
118
+ poster: { type: String, required: false, default: '' }, // Overridden with the playlist.poster if one is set there
119
+ preload: { type: String, required: false, default: '' },
120
+ captionsmenu: { type: Boolean, required: false, default: true }, // Show the captions below the video
121
+ playlistmenu: { type: Boolean, required: false, default: true }, // Show the playlist menu if there's multiple videos
122
+ playlistautoadvance: { type: Boolean, required: false, default: true }, // Play the next source group
123
+ playbackrates: {
124
+ type: Array,
125
+ required: false,
126
+ default: () => {
127
+ return [0.5, 1, 1.5, 2]
128
+ },
129
+ }, // Default playback speeds
130
+ },
131
+ watch: {},
132
+ computed: {
133
+ player() {
134
+ if (this.parseSourceType(this.current.src.sources) === 'youtube') {
135
+ return this.$refs.youtubePlayer
136
+ } else if (
137
+ this.parseSourceType(this.current.src.sources) === 'html5'
138
+ ) {
139
+ return this.$refs.html5Player
140
+ } else {
141
+ return null
142
+ }
143
+ },
144
+ current() {
145
+ const c = {
146
+ type: this.type,
147
+ attributes: this.attributes,
148
+ src: {},
149
+ }
150
+
151
+ if (this.src && this.src.sources && this.src.sources.length) {
152
+ c.src = this.src
153
+ return c
154
+ } else if (
155
+ this.playlist &&
156
+ this.playlist.length &&
157
+ typeof this.playlist[this.sourceIndex] !== 'undefined'
158
+ ) {
159
+ c.src = this.playlist[this.sourceIndex]
160
+ return c
161
+ } else {
162
+ return c
163
+ }
164
+ },
165
+ attributes() {
166
+ const attrs = {}
167
+
168
+ // Loop over all available props and get the value / default value
169
+ for (let i = 0; i < this.$options._propKeys.length; i++) {
170
+ let key = this.$options._propKeys[i]
171
+ attrs[key] = this[key]
172
+ }
173
+
174
+ return attrs
175
+ },
176
+ },
177
+ data() {
178
+ return {
179
+ sourceIndex: 0,
180
+ }
181
+ },
182
+ methods: {
183
+ onEnded(e) {
184
+ if (
185
+ this.playlistautoadvance &&
186
+ this.sourceIndex < this.playlist.length - 1
187
+ ) {
188
+ this.onPlaylistSelect(this.sourceIndex + 1)
189
+ this.$emit('ended', e)
190
+ }
191
+ },
192
+ onLoadeddata(e) {
193
+ // Loaded a new video
194
+ this.$emit('loadeddata', e)
195
+ },
196
+ onRemoteplayback(el) {
197
+ // Make sure the browser supports remote playback
198
+ if (!el.remote || !el.remote.watchAvailability) {
199
+ return false
200
+ }
201
+
202
+ el.remote
203
+ .prompt()
204
+ .then(() => {
205
+ alert('Remote playback not implemented on this player')
206
+ })
207
+ .catch((e) => {
208
+ //if(e.name == 'NotSupportedError') {}
209
+ alert(e.message)
210
+ })
211
+ },
212
+ onPictureInPicture(el) {
213
+ // Make sure the browser supports HTMLVideoElement.requestPictureInPicture()
214
+ if (!el.requestPictureInPicture) {
215
+ return false
216
+ }
217
+
218
+ if (!document.pictureInPictureElement) {
219
+ el.requestPictureInPicture()
220
+ this.$emit('click:pictureinpicture', true)
221
+ } else {
222
+ document.exitPictureInPicture()
223
+ this.$emit('click:pictureinpicture', false)
224
+ }
225
+ },
226
+ onFullscreen(el) {
227
+ // Make sure the browser supports document.fullscreenEnabled
228
+ if (!document.fullscreenEnabled) {
229
+ return false
230
+ }
231
+ if (!document.fullscreenElement) {
232
+ el.requestFullscreen()
233
+ this.$emit('click:fullscreen', true)
234
+ } else {
235
+ document.exitFullscreen()
236
+ this.$emit('click:fullscreen', false)
237
+ }
238
+ },
239
+ onPlaylistSelect(index) {
240
+ this.sourceIndex = parseInt(index)
241
+ this.player.load()
242
+ this.player.play()
243
+ },
244
+ parseSourceType(sources) {
245
+ const ytRegex =
246
+ /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
247
+
248
+ if (!sources || !sources.length || !sources[0].src) {
249
+ return null
250
+ }
251
+
252
+ // Parse the first src since any additional should be fallbacks of the same type
253
+ const src = sources[0].src
254
+ const type = sources[0].type
255
+
256
+ if (typeof type !== 'string') {
257
+ return null
258
+ } else if (src.match(ytRegex) || type === 'video/youtube') {
259
+ return 'youtube'
260
+ } else {
261
+ return 'html5'
262
+ }
263
+ },
264
+ },
265
+ beforeCreate() {},
266
+ beforeMount() {},
267
+ mounted() {},
268
+ beforeDestroy() {},
269
+ }
270
+ </script>
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Converts a number of seconds to a short duration format. Eg hh:mm:ss
3
+ *
4
+ * @param Integer seconds
5
+ *
6
+ * @param String The duration in "hh:mm:ss" format
7
+ */
8
+ const playerShortDuration = function (secondsParam) {
9
+ let ret = '--:--:--'
10
+
11
+ if (!isNaN(secondsParam) && secondsParam !== null) {
12
+ ret = ''
13
+ let seconds = parseInt(secondsParam, 10)
14
+
15
+ const hour = Math.floor(seconds / 60 / 60)
16
+ seconds = seconds - hour * 60 * 60 // Subtract it from seconds
17
+
18
+ const minute = Math.floor(seconds / 60)
19
+ seconds = Math.floor(seconds - minute * 60) // Subtract it from seconds
20
+
21
+ if (hour > 0) {
22
+ ret += String(hour).padStart(2, '0') + ':'
23
+ }
24
+
25
+ ret += String(minute).padStart(2, '0') + ':'
26
+ ret += String(seconds).padStart(2, '0')
27
+ }
28
+ return ret
29
+ }
30
+
31
+ export default { playerShortDuration }
@@ -0,0 +1,28 @@
1
+ export default {
2
+ locales: {
3
+ 'en-US': 'English',
4
+ 'sv-SE': 'Swedish',
5
+ },
6
+ playlist: {
7
+ up_next: 'Up Next',
8
+ default_name: 'Media',
9
+ previous: 'Play previous item in playlist',
10
+ next: 'Play next item in playlist',
11
+ },
12
+ player: {
13
+ playback_speed: 'Playback Speed',
14
+ playback_decrease: 'Decrease playback speed',
15
+ playback_increase: 'Increase playback speed',
16
+ toggle_settings: 'Toggle Settings',
17
+ download: 'Download',
18
+ toggle_remote_playback: 'Toggle Remote Playback',
19
+ toggle_picture_in_picture: 'Toggle Picture in Picture',
20
+ toggle_fullscreen: 'Toggle Fullscreen',
21
+ toggle_cc: 'Toggle closed captions',
22
+ volume_slider: 'Volume Slider',
23
+ rewind_10: 'Rewind 10 seconds',
24
+ play: 'Click to play',
25
+ pause: 'Click to pause',
26
+ no_support: "Sorry, your browser doesn't support embedded videos.",
27
+ },
28
+ }
@@ -0,0 +1,43 @@
1
+ import i18n from './index.js'
2
+
3
+ /**
4
+ * Traverses the properties of an object to return the localized value
5
+ *
6
+ * @param object obj The object to search
7
+ * @param string path The path in the object. Eg `a.b.c`
8
+ * @returns
9
+ */
10
+ function get(obj, path) {
11
+ const parts = path.split('.')
12
+ if (parts.length == 1) {
13
+ return obj[parts[0]]
14
+ }
15
+ if (typeof obj[parts[0]] === 'undefined') {
16
+ return null
17
+ }
18
+ return get(obj[parts[0]], parts.slice(1).join('.'))
19
+ }
20
+
21
+ const t = (lang, path) => {
22
+ if (typeof i18n[lang] === 'undefined') {
23
+ console.warn(
24
+ '[VuetifyPlayer] No support for locale ' +
25
+ lang +
26
+ '. Falling back to en-US'
27
+ )
28
+ lang = 'en-US'
29
+ }
30
+ let localized = get(i18n[lang], path)
31
+
32
+ // Could not localize this path. Return the path instead of null / undefined
33
+ if (!localized) {
34
+ localized = path
35
+ console.warn(
36
+ '[VuetifyPlayer] localization path ' + path + ' does not exist'
37
+ )
38
+ }
39
+
40
+ return localized
41
+ }
42
+
43
+ export { t }
@@ -0,0 +1,7 @@
1
+ import enUS from './en-US'
2
+ import svSE from './sv-SE'
3
+
4
+ export default {
5
+ 'en-US': enUS,
6
+ 'sv-SE': svSE,
7
+ }