@mindedge/vuetify-player 0.1.3 → 0.3.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,313 @@
1
+ <template>
2
+ <div>
3
+ <v-row>
4
+ <v-col :cols="playerCols">
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
+ @click:captions-expand="onClickCaptionsExpand"
44
+ @click:captions-paragraph="onClickCaptionsParagraph"
45
+ ></Html5Player>
46
+ </v-col>
47
+
48
+ <!-- Playlist stuff -->
49
+ <v-col
50
+ v-if="playlistmenu && playlist.length > 1"
51
+ :cols="playlistCols"
52
+ >
53
+ <PlaylistMenu
54
+ v-model="sourceIndex"
55
+ :language="language"
56
+ :playlist="playlist"
57
+ :poster="poster"
58
+ @click:select="onPlaylistSelect"
59
+ ></PlaylistMenu>
60
+ </v-col>
61
+ </v-row>
62
+ </div>
63
+ </template>
64
+
65
+ <script>
66
+ import YoutubePlayer from './Media/YoutubePlayer.vue'
67
+ import Html5Player from './Media/Html5Player.vue'
68
+ import PlaylistMenu from './Media/PlaylistMenu.vue'
69
+
70
+ export default {
71
+ name: 'VuetifyPlayer',
72
+ components: {
73
+ YoutubePlayer,
74
+ Html5Player,
75
+ PlaylistMenu,
76
+ },
77
+ props: {
78
+ language: { type: String, required: false, default: 'en-US' },
79
+ src: {
80
+ type: Object,
81
+ required: false,
82
+ default: () => {
83
+ return {}
84
+ },
85
+ },
86
+ playlist: {
87
+ type: Array,
88
+ required: false,
89
+ default: () => {
90
+ return []
91
+ },
92
+ },
93
+ type: { type: String, required: false, default: 'video' }, // Allowed video|audio. In audio mode the player has a max-height of 40px
94
+ autoplay: { type: Boolean, required: false, default: false }, // Autoplay on load. It's in the spec but DON'T USE THIS
95
+ autopictureinpicture: {
96
+ type: Boolean,
97
+ required: false,
98
+ default: false,
99
+ }, // Start with picture in picture mode
100
+ controls: { type: Boolean, required: false, default: true }, // Show video controls. When false only play/pause allowed but clicking on the video itself
101
+ controlslist: {
102
+ type: String,
103
+ required: false,
104
+ default: 'nodownload noremoteplayback',
105
+ }, // Space separated string per <video>. Allowed 'nodownload nofullscreen noremoteplayback'
106
+ crossorigin: { type: String, required: false, default: 'anonymous' },
107
+ disablepictureinpicture: {
108
+ type: Boolean,
109
+ required: false,
110
+ default: true,
111
+ }, // Shows the picture in picture button
112
+ disableremoteplayback: {
113
+ type: Boolean,
114
+ required: false,
115
+ default: true,
116
+ }, // Shows the remote playback button but functionality does not exist when clicked
117
+ height: { type: String, required: false, default: 'auto' },
118
+ width: { type: String, required: false, default: '100%' },
119
+ rewind: { type: Boolean, required: false, default: false }, // Enabled the rewind 10s button
120
+ loop: { type: Boolean, required: false, default: false }, // Loop the video on completion
121
+ muted: { type: Boolean, required: false, default: false }, // Start the video muted
122
+ playsinline: { Boolean: String, required: false, default: false }, // Force inline & disable fullscreen
123
+ poster: { type: String, required: false, default: '' }, // Overridden with the playlist.poster if one is set there
124
+ preload: { type: String, required: false, default: '' },
125
+ captionsmenu: { type: Boolean, required: false, default: true }, // Show the captions below the video
126
+ playlistmenu: { type: Boolean, required: false, default: true }, // Show the playlist menu if there's multiple videos
127
+ playlistautoadvance: { type: Boolean, required: false, default: true }, // Play the next source group
128
+ playbackrates: {
129
+ type: Array,
130
+ required: false,
131
+ default: () => {
132
+ return [0.5, 1, 1.5, 2]
133
+ },
134
+ }, // Default playback speeds
135
+ },
136
+ watch: {},
137
+ computed: {
138
+ player() {
139
+ if (this.parseSourceType(this.current.src.sources) === 'youtube') {
140
+ return this.$refs.youtubePlayer
141
+ } else if (
142
+ this.parseSourceType(this.current.src.sources) === 'html5'
143
+ ) {
144
+ return this.$refs.html5Player
145
+ } else {
146
+ return null
147
+ }
148
+ },
149
+ current() {
150
+ const c = {
151
+ type: this.type,
152
+ attributes: this.attributes,
153
+ src: {},
154
+ }
155
+
156
+ if (this.src && this.src.sources && this.src.sources.length) {
157
+ c.src = this.src
158
+ return c
159
+ } else if (
160
+ this.playlist &&
161
+ this.playlist.length &&
162
+ typeof this.playlist[this.sourceIndex] !== 'undefined'
163
+ ) {
164
+ c.src = this.playlist[this.sourceIndex]
165
+ return c
166
+ } else {
167
+ return c
168
+ }
169
+ },
170
+ attributes() {
171
+ const attrs = {}
172
+
173
+ // Loop over all available props and get the value / default value
174
+ for (let i = 0; i < this.$options._propKeys.length; i++) {
175
+ let key = this.$options._propKeys[i]
176
+ attrs[key] = this[key]
177
+ }
178
+
179
+ return attrs
180
+ },
181
+ playlistCols() {
182
+ // Captions collapsed, playlist will appear on the right
183
+ if (
184
+ !this.captionsExpanded &&
185
+ this.playlistmenu &&
186
+ this.playlist.length > 1
187
+ ) {
188
+ return 4
189
+ } else if (
190
+ this.captionsExpanded &&
191
+ this.playlistmenu &&
192
+ this.playlist.length > 1
193
+ ) {
194
+ // Captions expanded, playlist will appear as a new row on the bottom of everything
195
+ return 12
196
+ } else {
197
+ return 0
198
+ }
199
+ },
200
+ playerCols() {
201
+ if (
202
+ this.captionsExpanded ||
203
+ !this.playlistmenu ||
204
+ this.playlist.length <= 1
205
+ ) {
206
+ return 12
207
+ } else {
208
+ return 8
209
+ }
210
+ },
211
+ },
212
+ data() {
213
+ return {
214
+ sourceIndex: 0,
215
+ captionsExpanded: false,
216
+ }
217
+ },
218
+ methods: {
219
+ onEnded(e) {
220
+ if (
221
+ this.playlistautoadvance &&
222
+ this.sourceIndex < this.playlist.length - 1
223
+ ) {
224
+ this.onPlaylistSelect(this.sourceIndex + 1)
225
+ this.$emit('ended', e)
226
+ }
227
+ },
228
+ onLoadeddata(e) {
229
+ // Loaded a new video
230
+ this.$emit('loadeddata', e)
231
+ },
232
+ onRemoteplayback(el) {
233
+ // Make sure the browser supports remote playback
234
+ if (!el.remote || !el.remote.watchAvailability) {
235
+ return false
236
+ }
237
+
238
+ el.remote
239
+ .prompt()
240
+ .then(() => {
241
+ alert('Remote playback not implemented on this player')
242
+ })
243
+ .catch((e) => {
244
+ //if(e.name == 'NotSupportedError') {}
245
+ alert(e.message)
246
+ })
247
+ },
248
+ onPictureInPicture(el) {
249
+ // Make sure the browser supports HTMLVideoElement.requestPictureInPicture()
250
+ if (!el.requestPictureInPicture) {
251
+ return false
252
+ }
253
+
254
+ if (!document.pictureInPictureElement) {
255
+ el.requestPictureInPicture()
256
+ this.$emit('click:pictureinpicture', true)
257
+ } else {
258
+ document.exitPictureInPicture()
259
+ this.$emit('click:pictureinpicture', false)
260
+ }
261
+ },
262
+ onFullscreen(el) {
263
+ // Make sure the browser supports document.fullscreenEnabled
264
+ if (!document.fullscreenEnabled) {
265
+ return false
266
+ }
267
+ if (!document.fullscreenElement) {
268
+ el.requestFullscreen()
269
+ this.$emit('click:fullscreen', true)
270
+ } else {
271
+ document.exitFullscreen()
272
+ this.$emit('click:fullscreen', false)
273
+ }
274
+ },
275
+ onClickCaptionsExpand(expanded) {
276
+ this.captionsExpanded = expanded
277
+ this.$emit('click:captions-expand', expanded)
278
+ },
279
+ onClickCaptionsParagraph(isParagraph) {
280
+ this.$emit('click:captions-paragraph', isParagraph)
281
+ },
282
+ onPlaylistSelect(index) {
283
+ this.sourceIndex = parseInt(index)
284
+ this.player.load()
285
+ this.player.play()
286
+ },
287
+ parseSourceType(sources) {
288
+ const ytRegex =
289
+ /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
290
+
291
+ if (!sources || !sources.length || !sources[0].src) {
292
+ return null
293
+ }
294
+
295
+ // Parse the first src since any additional should be fallbacks of the same type
296
+ const src = sources[0].src
297
+ const type = sources[0].type
298
+
299
+ if (typeof type !== 'string') {
300
+ return null
301
+ } else if (src.match(ytRegex) || type === 'video/youtube') {
302
+ return 'youtube'
303
+ } else {
304
+ return 'html5'
305
+ }
306
+ },
307
+ },
308
+ beforeCreate() {},
309
+ beforeMount() {},
310
+ mounted() {},
311
+ beforeDestroy() {},
312
+ }
313
+ </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,34 @@
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
+ captions: {
29
+ expand: 'Expand',
30
+ collapse: 'Collapse',
31
+ view_as_paragraph: 'View as paragraph',
32
+ view_as_captions: 'View as captions',
33
+ },
34
+ }