├── index.html
├── readme.md
└── subtitles.js
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Subtitle
5 |
6 |
7 |
8 |
9 |
10 |
32 |
33 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Clappr Subtitle
2 |
3 | Simple Usage
4 |
5 | ```js
6 | var player = new Clappr.Player({
7 | source: 'video.mp4',
8 | baseUrl: '/latest',
9 | mute: true,
10 | height: 360,
11 | width: 640,
12 | plugins: [ClapprSubtitle],
13 | subtitle : "video.srt" // URL to subtitle
14 | });
15 | player.attachTo(document.getElementById('player'));
16 | ```
17 |
18 | Styling the subtitles
19 |
20 | ```js
21 | var player = new Clappr.Player({
22 | source: 'video.mp4',
23 | baseUrl: '/latest',
24 | mute: true,
25 | height: 360,
26 | width: 640,
27 | plugins: [ClapprSubtitle],
28 | subtitle : {
29 | src : "video.srt",
30 | auto : true, // automatically loads subtitle
31 | backgroundColor : 'transparent',
32 | fontWeight : 'normal',
33 | fontSize : '14px',
34 | color: 'yellow',
35 | textShadow : '1px 1px #000'
36 | },
37 | });
38 | player.attachTo(document.getElementById('player'));
39 | ```
40 |
41 | # SRT FORMAT
42 |
43 | ```
44 | 1
45 | 00:00:12,598 --> 00:00:13,891
46 | Do not try and bend the spoon. That's impossible. Instead... only try to realize the truth.
47 |
48 | 2
49 | 00:00:14,212 --> 00:00:17,846
50 | - What truth?
51 | - There is no spoon.
52 |
53 | 3
54 | 00:00:18,102 --> 00:00:24,317
55 | - There is no spoon?
56 | - Then you'll see, that it is not the spoon that bends, it is only yourself.
57 | ```
58 |
59 | # WARNING
60 |
61 | This is a very early version. You can't select subtitle tracks, and long srt files may run slow.
62 |
--------------------------------------------------------------------------------
/subtitles.js:
--------------------------------------------------------------------------------
1 | /*!
2 | *
3 | * ClapprSubtitle
4 | * Copyright 2016 JMV Technology. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * https://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License
17 | */
18 | (function() {
19 |
20 | var BLOCK_REGEX = /[0-9]+(?:\r\n|\r|\n)([0-9]{2}:[0-9]{2}:[0-9]{2}(?:,|\.)[0-9]{3}) --> ([0-9]{2}:[0-9]{2}:[0-9]{2}(?:,|\.)[0-9]{3})(?:\r\n|\r|\n)((?:.*(?:\r\n|\r|\n))*?)(?:\r\n|\r|\n)/g;
21 |
22 | window.ClapprSubtitle = Clappr.CorePlugin.extend({
23 |
24 | subtitles : [],
25 |
26 | element : null,
27 |
28 | active : false,
29 |
30 | options : {
31 | src : null,
32 | auto : false,
33 | backgroundColor : 'rgba(0, 0, 0, .8)',
34 | color : '#FFF',
35 | fontSize : '16px',
36 | fontWeight : 'bold',
37 | textShadow : 'rgb(0,0,0) 2px 2px 2px',
38 | },
39 |
40 | lastMediaControlButtonClick : new Date(),
41 |
42 | /**
43 | * @constructor
44 | */
45 | initialize : function() {
46 |
47 | this.subtitles = [];
48 |
49 | // register polyfills
50 | this.polyfill();
51 |
52 | // check options
53 | if (!'subtitle' in this._options)
54 | return;
55 |
56 | var options = this._options.subtitle;
57 |
58 | // override src and style
59 | // if 'options' is object
60 | if (typeof(options) === "object") {
61 | if('src' in options)
62 | this.options.src = options.src;
63 |
64 | if('auto' in options) {
65 | this.options.auto = options.auto === true;
66 | if(this.options.auto) {
67 | this.active = true;
68 | }
69 | }
70 |
71 | if('backgroundColor' in options)
72 | this.options.backgroundColor = options.backgroundColor;
73 |
74 | if('color' in options)
75 | this.options.color = options.color;
76 |
77 | if('fontSize' in options)
78 | this.options.fontSize = options.fontSize;
79 |
80 | if('fontWeight' in options)
81 | this.options.fontWeight = options.fontWeight;
82 |
83 | if('textShadow' in options)
84 | this.options.textShadow = options.textShadow;
85 |
86 | // override src if 'options' is string
87 | } else if (typeof(options) === "string") {
88 | this.options.src = options;
89 | this.options.auto = true;
90 | } else {
91 | return;
92 | }
93 |
94 | // initialize subtitle on DOM
95 | this.initializeElement();
96 |
97 | // fetch subtitles
98 | this.fetchSubtitle(this.onSubtitlesFetched.bind(this));
99 | },
100 |
101 | /**
102 | * Add event listeners
103 | */
104 | bindEvents : function() {
105 | this.listenTo(this.core, Clappr.Events.CORE_CONTAINERS_CREATED, this.containersCreated);
106 | this.listenTo(this.core.mediaControl, Clappr.Events.MEDIACONTROL_RENDERED, this.addButtonToMediaControl);
107 | this.listenTo(this.core.mediaControl, Clappr.Events.MEDIACONTROL_SHOW, this.onMediaControlShow);
108 | this.listenTo(this.core.mediaControl, Clappr.Events.MEDIACONTROL_HIDE, this.onMediaControlHide);
109 | this.listenTo(this.core.mediaControl, Clappr.Events.MEDIACONTROL_CONTAINERCHANGED, this.onContainerChanged);
110 | },
111 |
112 | /**
113 | * Add event listeners after containers were created
114 | */
115 | containersCreated : function() {
116 | // append element to container
117 | this.core.containers[0].$el.append(this.element);
118 | // run
119 | this.listenTo(this.core.containers[0].playback, Clappr.Events.PLAYBACK_TIMEUPDATE, this.run);
120 | },
121 |
122 | /**
123 | * On container changed
124 | */
125 | onContainerChanged : function() {
126 | // container changed is fired right off the bat
127 | // so we should bail if subtitles aren't yet loaded
128 | if (this.subtitles.length === 0)
129 | return;
130 |
131 | // kill the current element
132 | this.element.parentNode.removeChild(this.element);
133 |
134 | // clear subtitles
135 | this.subtitle = [];
136 |
137 | // initialize stuff again
138 | this.initialize();
139 |
140 | // trigger containers created
141 | this.containersCreated();
142 | },
143 |
144 | /**
145 | * Subtitles fetched
146 | */
147 | onSubtitlesFetched : function(data) {
148 | // parse subtitle
149 | this.parseSubtitle(data);
150 | },
151 |
152 | /**
153 | * Polyfill
154 | */
155 | polyfill : function() {
156 | if (!Array.prototype.find) {
157 | Array.prototype.find = function(predicate) {
158 | if (this === null) {
159 | throw new TypeError('Array.prototype.find called on null or undefined');
160 | }
161 | if (typeof predicate !== 'function') {
162 | throw new TypeError('predicate must be a function');
163 | }
164 | var list = Object(this);
165 | var length = list.length >>> 0;
166 | var thisArg = arguments[1];
167 | var value;
168 |
169 | for (var i = 0; i < length; i++) {
170 | value = list[i];
171 | if (predicate.call(thisArg, value, i, list)) {
172 | return value;
173 | }
174 | }
175 | return undefined;
176 | };
177 | }
178 | },
179 |
180 | /**
181 | * AJAX request to the subtitles source
182 | * @param {function} callback
183 | */
184 | fetchSubtitle : function(cb) {
185 | var r = new XMLHttpRequest();
186 | r.open("GET", this.options.src, true);
187 | r.onreadystatechange = function () {
188 | // nothing happens if request
189 | // fails or is not ready
190 | if (r.readyState != 4 || r.status != 200)
191 | return;
192 |
193 | // callback
194 | if(cb)
195 | cb(r.responseText);
196 | };
197 | r.send();
198 | },
199 |
200 | /**
201 | * Parse subtitle
202 | * @param {string} data
203 | */
204 | parseSubtitle : function(datas) {
205 |
206 | // Get blocks and loop through them
207 | blocks = datas.match(BLOCK_REGEX);
208 |
209 | for(var i = 0; i < blocks.length; i++) {
210 |
211 | var startTime = null;
212 | var endTime = null;
213 | var text = "";
214 |
215 | // Break the block in lines
216 | var block = blocks[i];
217 | var lines = block.split(/(?:\r\n|\r|\n)/);
218 |
219 | // The second line is the time line.
220 | // We parse the start and end time.
221 | var time = lines[1].split(' --> ');
222 | var startTime = this.humanDurationToSeconds(time[0].trim());
223 | var endTime = this.humanDurationToSeconds(time[1].trim());
224 |
225 | // As for the rest of the lines, we loop through
226 | // them and append the to the text,
227 | for (var j = 2; j < lines.length; j++) {
228 | var line = lines[j].trim();
229 |
230 | if (text.length > 0) {
231 | text += "
";
232 | }
233 |
234 | text += line;
235 | }
236 |
237 | // Then we push it to the subtitles
238 | this.subtitles.push({
239 | startTime: startTime,
240 | endTime: endTime,
241 | text: text
242 | });
243 | }
244 | },
245 |
246 | /**
247 | * Converts human duration time (00:00:00) to seconds
248 | * @param {string} human time
249 | * @return {float}
250 | */
251 | humanDurationToSeconds : function(duration) {
252 | duration = duration.split(":");
253 |
254 | var hours = duration[0],
255 | minutes = duration[1],
256 | seconds = duration[2].replace(",", ".");
257 |
258 | var result = 0.00;
259 | result += parseFloat(hours) * 60 * 60;
260 | result += parseFloat(minutes) * 60;
261 | result += parseFloat(seconds);
262 |
263 | return result;
264 | },
265 |
266 | /**
267 | * Initializes the subtitle on the dom
268 | */
269 | initializeElement : function() {
270 | var el = document.createElement('div');
271 | el.style.display = 'block';
272 | el.style.position = 'absolute';
273 | el.style.left = '50%';
274 | el.style.bottom = '50px';
275 | el.style.color = this.options.color;
276 | el.style.backgroundColor = this.options.backgroundColor;
277 | el.style.fontSize = this.options.fontSize;
278 | el.style.fontWeight = this.options.fontWeight;
279 | el.style.textShadow = this.options.textShadow;
280 | el.style.transform = 'translateX(-50%)';
281 | el.style.boxSizing = 'border-box';
282 | el.style.padding = '7px';
283 | el.style.opacity = '0';
284 | el.style.pointerEvents = 'none';
285 | el.style.maxWidth = '90%';
286 | el.style.whiteSpace = 'normal';
287 | el.style.zIndex = 1;
288 | this.element = el;
289 | },
290 |
291 | /**
292 | * Add button to media control
293 | */
294 | addButtonToMediaControl : function() {
295 | var bar = this.core
296 | .mediaControl
297 | .$el
298 | .children('.media-control-layer')
299 | .children('.media-control-right-panel');
300 |
301 | // create icon
302 | var button = document.createElement('button');
303 | button.classList.add('media-control-button');
304 | button.classList.add('media-control-icon');
305 | button.classList.add('media-control-subtitle-toggler');
306 | button.innerHTML = this.getMediaControlButtonSVG();
307 |
308 | // if active, glow
309 | if(this.active)
310 | button.style.opacity = '1';
311 |
312 | // append to bar
313 | bar.append(button);
314 |
315 | // add listener
316 | this.core.$el.on('click', this.onMediaControlButtonClick.bind(this));
317 | },
318 |
319 | /**
320 | * Button SGV
321 | * @return {string}
322 | */
323 | getMediaControlButtonSVG : function() {
324 | return '';
328 | },
329 |
330 | /**
331 | * on button click
332 | */
333 | onMediaControlButtonClick : function(mouseEvent) {
334 | // this is a bit of a hack
335 | // it's ugly, I know, but I have yet to figure out how the click events work
336 | // so I'm bailing if click's target does not have the class 'media-control-subtitle-toggler'
337 | // I used this class 'media-control-subtitle-toggler' to identify the right element
338 | if(!mouseEvent.target.classList.contains('media-control-subtitle-toggler'))
339 | return;
340 |
341 | // This is also a bit of a hack.
342 | // We are preventing double clicks by checking the time the last click happened
343 | // if it's less then 300 miliseconds ago, we bail
344 | if (new Date() - this.lastMediaControlButtonClick < 300)
345 | return;
346 |
347 | // update lastMediaControlButtonClick
348 | this.lastMediaControlButtonClick = new Date();
349 |
350 | // toggle active on/off
351 | if(this.active) {
352 | this.active = false;
353 | mouseEvent.target.style.opacity = '.5';
354 | this.hideElement();
355 | } else {
356 | this.active = true;
357 | mouseEvent.target.style.opacity = '1';
358 | }
359 | },
360 |
361 |
362 | /**
363 | * Hides the subtitle element
364 | */
365 | hideElement : function() {
366 | this.element.style.opacity = '0';
367 | },
368 |
369 | /**
370 | * Shows the subtitle element with text
371 | * @param {string} text
372 | */
373 | showElement : function(text) {
374 | if(!this.active)
375 | return;
376 | this.element.innerHTML = text;
377 | this.element.style.opacity = '1';
378 | },
379 |
380 | /**
381 | * Subtitle element moves up
382 | * to give space to the controls
383 | */
384 | onMediaControlShow : function() {
385 | if(this.element)
386 | this.element.style.bottom = '100px';
387 | },
388 |
389 | /**
390 | * Subtitle element moves down
391 | * when controls hide
392 | */
393 | onMediaControlHide : function() {
394 | if(this.element)
395 | this.element.style.bottom = '50px';
396 | },
397 |
398 | /**
399 | * Show subtitles as media is playing
400 | */
401 | run : function(time) {
402 | var subtitle = this.subtitles.find(function(subtitle) {
403 | return time.current >= subtitle.startTime && time.current <= subtitle.endTime
404 | });
405 |
406 | if(subtitle) {
407 | this.showElement(subtitle.text);
408 | } else {
409 | this.hideElement();
410 | }
411 | },
412 |
413 | });
414 | })();
415 |
--------------------------------------------------------------------------------