@mindedge/vuetify-player 0.2.0 → 0.3.1
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.
- package/README.md +46 -44
- package/package.json +2 -2
- package/src/components/Media/CaptionsMenu.vue +230 -34
- package/src/components/Media/Html5Player.vue +721 -502
- package/src/components/VuetifyPlayer.vue +45 -2
- package/src/i18n/en-US.js +7 -0
- package/src/i18n/es-ES.js +35 -0
- package/src/i18n/index.js +2 -0
- package/src/i18n/sv-SE.js +7 -0
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ An accessible, localized, full featured media player with Vuetifyjs
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
7
|
- [Quick Start](#quick-start)
|
|
8
|
-
- [View demos locally](#local-project-setup-to-view-demos)
|
|
9
8
|
- [Complete media source structure](#full-media-src-structure)
|
|
10
9
|
- [Define a media source](#the-src-attribute)
|
|
11
10
|
- [Define a playlist](#the-playlist-attribute)
|
|
12
11
|
- [Define ads / preroll / postroll](#the-ads-array)
|
|
13
12
|
- [Supported Attributes](#supported-vuetifyplayer-attributes)
|
|
14
13
|
- [Supported Events](#supported-vuetifyplayer-events)
|
|
14
|
+
- [Captions](#captions)
|
|
15
15
|
- [License](#license)
|
|
16
16
|
|
|
17
17
|
---
|
|
@@ -60,26 +60,6 @@ src: {
|
|
|
60
60
|
|
|
61
61
|
### 4. Enjoy~
|
|
62
62
|
|
|
63
|
-
## Local project setup to view demos
|
|
64
|
-
|
|
65
|
-
Clone the repo
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
git clone https://github.com/mindedge/vuetify-player.git
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Install necessary packages
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
npm install
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Compile and serve. This also hot-reloads for development & testing
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
npm run serve
|
|
81
|
-
```
|
|
82
|
-
|
|
83
63
|
## Full media `src` structure
|
|
84
64
|
|
|
85
65
|
```javascript
|
|
@@ -238,29 +218,51 @@ See [Full media `src` structure for where the ads array is placed](#full-media-s
|
|
|
238
218
|
|
|
239
219
|
## Supported `<VuetifyPlayer>` Events
|
|
240
220
|
|
|
241
|
-
| Event name
|
|
242
|
-
|
|
|
243
|
-
| `abort`
|
|
244
|
-
| `canplay`
|
|
245
|
-
| `canplaythrough`
|
|
246
|
-
| `emptied`
|
|
247
|
-
| `ended`
|
|
248
|
-
| `error`
|
|
249
|
-
| `loadeddata`
|
|
250
|
-
| `loadedmetadata`
|
|
251
|
-
| `play`
|
|
252
|
-
| `pause`
|
|
253
|
-
| `progress`
|
|
254
|
-
| `seeking`
|
|
255
|
-
| `timeupdate`
|
|
256
|
-
| `ratechange`
|
|
257
|
-
| `stalled`
|
|
258
|
-
| `volumechange`
|
|
259
|
-
| `waiting`
|
|
260
|
-
| `click:fullscreen`
|
|
261
|
-
| `click:pictureinpicture`
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
221
|
+
| Event name | Returns | When it's triggered |
|
|
222
|
+
| -------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `abort` | `Event` | Download interrupted |
|
|
224
|
+
| `canplay` | `Event` | Playback can start |
|
|
225
|
+
| `canplaythrough` | `Event` | Playback can continue and should not be interrupted. Readstate is 3 |
|
|
226
|
+
| `emptied` | `Event` | The network connection is down |
|
|
227
|
+
| `ended` | `Event` | When playback has stopped because the end of the media was reached |
|
|
228
|
+
| `error` | `Event` | A network error occurred during the download |
|
|
229
|
+
| `loadeddata` | `Event` | When the frame at the current playback position of the media has finished loading; often the first frame |
|
|
230
|
+
| `loadedmetadata` | `Event` | When the metadata has been loaded |
|
|
231
|
+
| `play` | `Event` | The media has received a request to start playing |
|
|
232
|
+
| `pause` | `Event` | Playback has been suspended |
|
|
233
|
+
| `progress` | `Event` | The progress event is fired periodically as the browser loads a resource. |
|
|
234
|
+
| `seeking` | `Event` | Playback has moved to a new location |
|
|
235
|
+
| `timeupdate` | `Object` | The current time was changed. Object contains { event: Event, current_percent: Number } |
|
|
236
|
+
| `ratechange` | `Number` | The playback speed multiplier |
|
|
237
|
+
| `stalled` | `Event` | The browser tried to download but has not received data yet |
|
|
238
|
+
| `volumechange` | `Number` | The volume or muted button changed. Value from 0.0 to 1 |
|
|
239
|
+
| `waiting` | `Event` | Pause playback to download more data |
|
|
240
|
+
| `click:fullscreen` | `true` \| `false` | When the fullscreen button is clicked. true on fullscreen, false on exiting fullscreen |
|
|
241
|
+
| `click:pictureinpicture` | `true` \| `false` | When the picture-in-picture button is clicked. true on enabled, false on disabled |
|
|
242
|
+
| `click:captions-expand` | `true` \| `false` | When the expand captions button is clicked. true on expanded, false on collapsed |
|
|
243
|
+
| `click:captions-paragraph` | `true` \| `false` | When the view as paragraph button is clicked. true when viewing as a paragraph, false when viewing as timed captions |
|
|
244
|
+
| `mouseover` | `MouseEvent` | Mouse over the media |
|
|
245
|
+
| `mouseout` | `MouseEvent` | Mouse left the media |
|
|
246
|
+
|
|
247
|
+
## Captions
|
|
248
|
+
|
|
249
|
+
The player supports `.vtt` captions as defined in the `tracks` array explained above.
|
|
250
|
+
|
|
251
|
+
Additionally we support the tag `<c.transcript> ... </c>` inline with your captions text. This tag will omit the enclosed text from the video player captions overlay but show the text in the separate interactive captions panel.
|
|
252
|
+
This allows you to include additional information that you want to appear in the transcript that might not be appropriate to display in the video player itself.
|
|
253
|
+
|
|
254
|
+
Below is a `sample.vtt` on how to use the `<c.transcript>` tag.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
WEBVTT
|
|
258
|
+
|
|
259
|
+
00:00:00.000 --> 00:00:03.999
|
|
260
|
+
This text will show. <c.transcript>This text is hidden from the player.</c> This is some more text to show.
|
|
261
|
+
This text will show up on a new line in the player.
|
|
262
|
+
|
|
263
|
+
00:00:03.000 --> 00:00:5.999
|
|
264
|
+
sentence here to break it up
|
|
265
|
+
```
|
|
264
266
|
|
|
265
267
|
## License
|
|
266
268
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindedge/vuetify-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Accessible, localized, full featured media player with Vuetifyjs",
|
|
6
6
|
"author": "Jacob Rogaishio",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test
|
|
8
|
+
"test": "vue-cli-service test:unit",
|
|
9
9
|
"lint": "vue-cli-service lint",
|
|
10
10
|
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\"",
|
|
11
11
|
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore ."
|
|
@@ -1,32 +1,129 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-card>
|
|
3
|
+
<v-card-actions class="justify-end">
|
|
4
|
+
<v-tooltip top>
|
|
5
|
+
<template v-slot:activator="{ on, attrs }">
|
|
6
|
+
<v-btn
|
|
7
|
+
color="primary"
|
|
8
|
+
text
|
|
9
|
+
v-bind="attrs"
|
|
10
|
+
v-on="on"
|
|
11
|
+
@click="onClickToggleParagraphView"
|
|
12
|
+
>
|
|
13
|
+
<v-icon>{{
|
|
14
|
+
paragraphView
|
|
15
|
+
? 'mdi-closed-caption-outline'
|
|
16
|
+
: 'mdi-text-box-outline'
|
|
17
|
+
}}</v-icon>
|
|
18
|
+
<span class="sr-only">{{
|
|
19
|
+
paragraphView
|
|
20
|
+
? t(language, 'captions.view_as_captions')
|
|
21
|
+
: t(language, 'captions.view_as_paragraph')
|
|
22
|
+
}}</span>
|
|
23
|
+
</v-btn></template
|
|
24
|
+
>
|
|
25
|
+
<span>{{
|
|
26
|
+
paragraphView
|
|
27
|
+
? t(language, 'captions.view_as_captions')
|
|
28
|
+
: t(language, 'captions.view_as_paragraph')
|
|
29
|
+
}}</span>
|
|
30
|
+
</v-tooltip>
|
|
31
|
+
<v-tooltip top>
|
|
32
|
+
<template v-slot:activator="{ on, attrs }">
|
|
33
|
+
<v-btn
|
|
34
|
+
color="primary"
|
|
35
|
+
text
|
|
36
|
+
v-bind="attrs"
|
|
37
|
+
v-on="on"
|
|
38
|
+
@click="onClickToggleExpand"
|
|
39
|
+
>
|
|
40
|
+
<v-icon>{{
|
|
41
|
+
expanded ? 'mdi-arrow-collapse' : 'mdi-arrow-expand'
|
|
42
|
+
}}</v-icon>
|
|
43
|
+
<span class="sr-only">{{
|
|
44
|
+
expanded
|
|
45
|
+
? t(language, 'captions.collapse')
|
|
46
|
+
: t(language, 'captions.expand')
|
|
47
|
+
}}</span>
|
|
48
|
+
</v-btn></template
|
|
49
|
+
>
|
|
50
|
+
<span>{{
|
|
51
|
+
expanded
|
|
52
|
+
? t(language, 'captions.collapse')
|
|
53
|
+
: t(language, 'captions.expand')
|
|
54
|
+
}}</span>
|
|
55
|
+
</v-tooltip>
|
|
56
|
+
</v-card-actions>
|
|
3
57
|
<v-card-text>
|
|
4
|
-
<v-list ref="captionList" class="
|
|
58
|
+
<v-list ref="captionList" :class="captionsList">
|
|
5
59
|
<v-list-item-group v-model="captionIndex">
|
|
6
60
|
<v-list-item
|
|
7
61
|
ref="captionItems"
|
|
8
|
-
v-for="(cue, index) in
|
|
62
|
+
v-for="(cue, index) in cues"
|
|
9
63
|
:key="index"
|
|
64
|
+
:two-line="expanded"
|
|
10
65
|
@click="onCueClick(cue.startTime)"
|
|
11
66
|
>
|
|
12
|
-
<v-
|
|
13
|
-
<v-icon
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
v-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
<template v-if="!expanded">
|
|
68
|
+
<v-list-item-icon v-if="!paragraphView">
|
|
69
|
+
<v-icon
|
|
70
|
+
>{{
|
|
71
|
+
index === captionIndex
|
|
72
|
+
? 'mdi-arrow-right-drop-circle-outline'
|
|
73
|
+
: 'mdi-checkbox-blank-circle-outline'
|
|
74
|
+
}}
|
|
75
|
+
</v-icon>
|
|
76
|
+
</v-list-item-icon>
|
|
77
|
+
<v-list-item-content>
|
|
78
|
+
<v-list-item-title
|
|
79
|
+
v-html="cue.rawText || cue.text"
|
|
80
|
+
class="caption-text"
|
|
81
|
+
></v-list-item-title>
|
|
82
|
+
</v-list-item-content>
|
|
83
|
+
<v-list-item-action v-if="!paragraphView">
|
|
84
|
+
<span aria-hidden="true">
|
|
85
|
+
{{
|
|
86
|
+
filters.playerShortDuration(
|
|
87
|
+
cue.startTime
|
|
88
|
+
)
|
|
89
|
+
}}
|
|
90
|
+
-
|
|
91
|
+
{{
|
|
92
|
+
filters.playerShortDuration(cue.endTime)
|
|
93
|
+
}}
|
|
94
|
+
</span>
|
|
95
|
+
</v-list-item-action>
|
|
96
|
+
</template>
|
|
97
|
+
<template v-if="expanded">
|
|
98
|
+
<v-list-item-content>
|
|
99
|
+
<v-list-item-title
|
|
100
|
+
v-html="cue.rawText || cue.text"
|
|
101
|
+
class="caption-text"
|
|
102
|
+
></v-list-item-title>
|
|
103
|
+
<v-list-item-subtitle v-if="!paragraphView">
|
|
104
|
+
<v-icon
|
|
105
|
+
>{{
|
|
106
|
+
index === captionIndex
|
|
107
|
+
? 'mdi-arrow-right-drop-circle-outline'
|
|
108
|
+
: 'mdi-checkbox-blank-circle-outline'
|
|
109
|
+
}}
|
|
110
|
+
</v-icon>
|
|
111
|
+
<span aria-hidden="true">
|
|
112
|
+
{{
|
|
113
|
+
filters.playerShortDuration(
|
|
114
|
+
cue.startTime
|
|
115
|
+
)
|
|
116
|
+
}}
|
|
117
|
+
-
|
|
118
|
+
{{
|
|
119
|
+
filters.playerShortDuration(
|
|
120
|
+
cue.endTime
|
|
121
|
+
)
|
|
122
|
+
}}
|
|
123
|
+
</span>
|
|
124
|
+
</v-list-item-subtitle>
|
|
125
|
+
</v-list-item-content>
|
|
126
|
+
</template>
|
|
30
127
|
</v-list-item>
|
|
31
128
|
</v-list-item-group>
|
|
32
129
|
</v-list>
|
|
@@ -36,17 +133,97 @@
|
|
|
36
133
|
|
|
37
134
|
<script>
|
|
38
135
|
import filters from '../filters'
|
|
136
|
+
import { t } from '../../i18n/i18n'
|
|
39
137
|
|
|
40
138
|
export default {
|
|
41
139
|
props: {
|
|
42
|
-
value: { type: Object, required: true },
|
|
140
|
+
value: { type: [Object, Array], required: true },
|
|
43
141
|
language: { type: String, required: false, default: 'en-US' },
|
|
44
142
|
},
|
|
143
|
+
computed: {
|
|
144
|
+
captionsList() {
|
|
145
|
+
return !this.expanded
|
|
146
|
+
? 'captions-list captions-list--state-collapsed'
|
|
147
|
+
: 'captions-list captions-list--state-expanded'
|
|
148
|
+
},
|
|
149
|
+
cues() {
|
|
150
|
+
// Normal cues view
|
|
151
|
+
if (
|
|
152
|
+
typeof this.captions.cues !== 'undefined' &&
|
|
153
|
+
!this.paragraphView
|
|
154
|
+
) {
|
|
155
|
+
return this.captions.cues
|
|
156
|
+
} else if (
|
|
157
|
+
typeof this.captions.cues !== 'undefined' &&
|
|
158
|
+
this.paragraphView
|
|
159
|
+
) {
|
|
160
|
+
// Paragraph view
|
|
161
|
+
let cues = this.captions.cues
|
|
162
|
+
const paragraphs = []
|
|
163
|
+
let puncuationCount = 0
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < cues.length; i++) {
|
|
166
|
+
// Add the first item. Use `new VTTCue` to break the reference
|
|
167
|
+
if (paragraphs.length === 0) {
|
|
168
|
+
paragraphs.push(
|
|
169
|
+
new VTTCue(
|
|
170
|
+
cues[i].startTime,
|
|
171
|
+
cues[i].endTime,
|
|
172
|
+
cues[i].text
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
// Skip first element
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Increment the count on puncuation checks
|
|
180
|
+
if (new RegExp(/[.?!]/).test(cues[i].text)) {
|
|
181
|
+
puncuationCount++
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create a new paragraph every 3 sentences
|
|
185
|
+
if (puncuationCount > 3) {
|
|
186
|
+
// Find the first puncuation and include it in the slice
|
|
187
|
+
const breakIndex = cues[i].text.search(/[.?!]/) + 1
|
|
188
|
+
|
|
189
|
+
// Append the first part to the previous paragraph so it ends on a period
|
|
190
|
+
paragraphs[paragraphs.length - 1].text +=
|
|
191
|
+
' ' + cues[i].text.slice(0, breakIndex)
|
|
192
|
+
|
|
193
|
+
// Use `new VTTCue` to break the reference. Otherwise the below appends will duplicate text
|
|
194
|
+
// Also grab from the breakIndex afterwards to get the potential next sentence
|
|
195
|
+
paragraphs.push(
|
|
196
|
+
new VTTCue(
|
|
197
|
+
cues[i].startTime,
|
|
198
|
+
cues[i].endTime,
|
|
199
|
+
cues[i].text.slice(breakIndex).trim()
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
puncuationCount = 0
|
|
203
|
+
} else {
|
|
204
|
+
// Append the cue text and update the end time
|
|
205
|
+
paragraphs[paragraphs.length - 1].endTime =
|
|
206
|
+
cues[i].endTime
|
|
207
|
+
paragraphs[paragraphs.length - 1].text +=
|
|
208
|
+
' ' + cues[i].text
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return paragraphs
|
|
213
|
+
} else {
|
|
214
|
+
// No cues found!
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
},
|
|
45
219
|
data() {
|
|
46
220
|
return {
|
|
221
|
+
t,
|
|
47
222
|
filters,
|
|
48
223
|
captions: {},
|
|
49
224
|
captionIndex: 0,
|
|
225
|
+
expanded: false,
|
|
226
|
+
paragraphView: false,
|
|
50
227
|
}
|
|
51
228
|
},
|
|
52
229
|
watch: {
|
|
@@ -59,21 +236,14 @@ export default {
|
|
|
59
236
|
},
|
|
60
237
|
},
|
|
61
238
|
methods: {
|
|
62
|
-
cueKey(cue) {
|
|
63
|
-
const str =
|
|
64
|
-
cue.language +
|
|
65
|
-
cue.startTime.toString() +
|
|
66
|
-
cue.endTime.toString() +
|
|
67
|
-
cue.text
|
|
68
|
-
return str.split('').reduce(function (a, b) {
|
|
69
|
-
a = (a << 5) - a + b.charCodeAt(0)
|
|
70
|
-
return a & a
|
|
71
|
-
}, 0)
|
|
72
|
-
},
|
|
73
239
|
currentCue(captions) {
|
|
74
240
|
let currentIndex = 0
|
|
75
241
|
|
|
76
|
-
if (
|
|
242
|
+
if (
|
|
243
|
+
typeof captions.cues !== 'undefined' &&
|
|
244
|
+
typeof captions.activeCues !== 'undefined' &&
|
|
245
|
+
captions.activeCues.length
|
|
246
|
+
) {
|
|
77
247
|
for (let i = 0; i < captions.cues.length; i++) {
|
|
78
248
|
const cue = captions.cues[i]
|
|
79
249
|
if (captions.activeCues[0].startTime === cue.startTime) {
|
|
@@ -103,6 +273,14 @@ export default {
|
|
|
103
273
|
onCueClick(time) {
|
|
104
274
|
this.$emit('click:cue', time)
|
|
105
275
|
},
|
|
276
|
+
onClickToggleExpand() {
|
|
277
|
+
this.expanded = !this.expanded
|
|
278
|
+
this.$emit('click:expand', this.expanded)
|
|
279
|
+
},
|
|
280
|
+
onClickToggleParagraphView() {
|
|
281
|
+
this.paragraphView = !this.paragraphView
|
|
282
|
+
this.$emit('click:paragraph', this.paragraphView)
|
|
283
|
+
},
|
|
106
284
|
},
|
|
107
285
|
mounted() {
|
|
108
286
|
this.captions = this.value
|
|
@@ -113,8 +291,10 @@ export default {
|
|
|
113
291
|
|
|
114
292
|
<style scoped>
|
|
115
293
|
.captions-list {
|
|
116
|
-
max-height: 10em;
|
|
117
294
|
overflow-y: scroll;
|
|
295
|
+
}
|
|
296
|
+
.captions-list--state-collapsed {
|
|
297
|
+
max-height: 10em;
|
|
118
298
|
/* Fade the top/bottom 20% effect. The "red" mask is so the scrollbar doesn't get this effect*/
|
|
119
299
|
mask: linear-gradient(90deg, rgba(255, 0, 0, 0) 98%, rgba(255, 0, 0, 1) 98%),
|
|
120
300
|
linear-gradient(
|
|
@@ -125,4 +305,20 @@ export default {
|
|
|
125
305
|
rgba(0, 0, 0, 0) 100%
|
|
126
306
|
);
|
|
127
307
|
}
|
|
308
|
+
.captions-list--state-expanded {
|
|
309
|
+
aspect-ratio: 16 / 9;
|
|
310
|
+
/* Fade the top/bottom 20% effect. The "red" mask is so the scrollbar doesn't get this effect*/
|
|
311
|
+
mask: linear-gradient(90deg, rgba(255, 0, 0, 0) 98%, rgba(255, 0, 0, 1) 98%),
|
|
312
|
+
linear-gradient(
|
|
313
|
+
0deg,
|
|
314
|
+
rgba(0, 0, 0, 0) 0%,
|
|
315
|
+
rgba(0, 0, 0, 1) 5%,
|
|
316
|
+
rgba(0, 0, 0, 1) 95%,
|
|
317
|
+
rgba(0, 0, 0, 0) 100%
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
.caption-text {
|
|
321
|
+
overflow: visible;
|
|
322
|
+
white-space: initial;
|
|
323
|
+
}
|
|
128
324
|
</style>
|