@checksub_team/peaks_timeline 1.4.17

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/src/player.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link Player} class.
5
+ *
6
+ * @module player
7
+ */
8
+
9
+ define([
10
+ './utils'
11
+ ], function(Utils) {
12
+ 'use strict';
13
+
14
+ /**
15
+ * A wrapper for interfacing with the HTML5 media element API.
16
+ * Initializes the player for a given media element.
17
+ *
18
+ * @class
19
+ * @alias Player
20
+ *
21
+ * @param {Peaks} peaks The parent {@link Peaks} object.
22
+ */
23
+
24
+ function Player(peaks) {
25
+ var self = this;
26
+
27
+ self._peaks = peaks;
28
+ self._currentTime = 0;
29
+ self._isPlaying = false;
30
+
31
+ self._speed = 1;
32
+
33
+ self._segmentPlayInterval = null;
34
+ }
35
+
36
+ /**
37
+ * Set the playing speed.
38
+ *
39
+ * @param {Number} newSpeed The new speed, 1.0 is the normal (and default) speed.
40
+ */
41
+
42
+ Player.prototype.setSpeed = function(newSpeed) {
43
+ this._speed = newSpeed;
44
+ };
45
+
46
+ /**
47
+ * Cleans up the player object, removing all event listeners from the
48
+ * associated media element.
49
+ */
50
+
51
+ Player.prototype.destroy = function() {
52
+ if (this._segmentPlayInterval !== null) {
53
+ clearInterval(this._segmentPlayInterval);
54
+ this._segmentPlayInterval = null;
55
+ }
56
+
57
+ if (this._playInterval !== null) {
58
+ clearInterval(this._playInterval);
59
+ this._playInterval = null;
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Starts playback.
65
+ */
66
+
67
+ Player.prototype.play = function() {
68
+ if (!this._playInterval) {
69
+ var prevTime = Date.now();
70
+ var time;
71
+ var self = this;
72
+
73
+ this._isPlaying = true;
74
+
75
+ this._peaks.overrideInteractions(true, false);
76
+
77
+ this._playInterval = setInterval(function() {
78
+ time = Date.now();
79
+ self._currentTime += ((time - prevTime) / 1000) * self._speed;
80
+ prevTime = time;
81
+ self._peaks.emit('timeline.update', self._currentTime);
82
+ }, 10);
83
+
84
+ this._peaks.emit('timeline.play', this._currentTime);
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Pauses playback.
90
+ */
91
+
92
+ Player.prototype.pause = function() {
93
+ if (this._playInterval) {
94
+ this._isPlaying = false;
95
+
96
+ clearInterval(this._playInterval);
97
+ this._playInterval = null;
98
+
99
+ this._peaks.overrideInteractions(false);
100
+
101
+ this._peaks.emit('timeline.pause', this._currentTime);
102
+ }
103
+ };
104
+
105
+ /**
106
+ * @returns {Boolean} <code>true</code> if playing, <code>false</code>
107
+ * otherwise.
108
+ */
109
+
110
+ Player.prototype.isPlaying = function() {
111
+ return this._isPlaying;
112
+ };
113
+
114
+ /**
115
+ * Returns the current playback time position, in seconds.
116
+ *
117
+ * @returns {Number}
118
+ */
119
+
120
+ Player.prototype.getCurrentTime = function() {
121
+ return this._currentTime;
122
+ };
123
+
124
+ /**
125
+ * Seeks to a given time position within the media.
126
+ *
127
+ * @param {Number} time The time position, in seconds.
128
+ */
129
+
130
+ Player.prototype.seek = function(time) {
131
+ if (!Utils.isValidTime(time)) {
132
+ this._peaks.logger('peaks.player.seek(): parameter must be a valid time, in seconds');
133
+ return;
134
+ }
135
+
136
+ this._currentTime = Math.max(0, time);
137
+
138
+ this._peaks.emit('timeline.seek', this._currentTime);
139
+ this._peaks.emit('timeline.update', this._currentTime);
140
+ };
141
+
142
+ /**
143
+ * Plays the given segment.
144
+ *
145
+ * @param {Segment} segment The segment denoting the time region to play.
146
+ */
147
+
148
+ Player.prototype.playSegment = function(segment) {
149
+ var self = this;
150
+
151
+ if (!segment ||
152
+ !Utils.isValidTime(segment.startTime) ||
153
+ !Utils.isValidTime(segment.endTime)) {
154
+ self._peaks.logger('peaks.player.playSegment(): parameter must be a segment object');
155
+ return;
156
+ }
157
+
158
+ clearInterval(self._segmentPlayInterval);
159
+ self._segmentPlayInterval = null;
160
+
161
+ // Set time to segment start time
162
+ self.seek(segment.startTime);
163
+
164
+ // Start playing
165
+ self.play();
166
+
167
+ self._segmentPlayInterval = setInterval(function() {
168
+ if (self.getCurrentTime() >= segment.endTime || !self._isPlaying) {
169
+ clearInterval(self._segmentPlayInterval);
170
+ self._segmentPlayInterval = null;
171
+ self.pause();
172
+ // self.seek(segment.endTime);
173
+ }
174
+ }, 10);
175
+ };
176
+
177
+ return Player;
178
+ });
@@ -0,0 +1,413 @@
1
+ /**
2
+ * @file
3
+ *
4
+ * Defines the {@link PlayheadLayer} class.
5
+ *
6
+ * @module playhead-layer
7
+ */
8
+
9
+ define([
10
+ './utils',
11
+ 'konva'
12
+ ], function(Utils, Konva) {
13
+ 'use strict';
14
+
15
+ var HANDLE_RADIUS = 10;
16
+
17
+ /**
18
+ * Creates a Konva.Layer that displays a playhead marker.
19
+ *
20
+ * @class
21
+ * @alias PlayheadLayer
22
+ *
23
+ * @param {Peaks} peaks
24
+ * @param {WaveformOverview|WaveformZoomView} view
25
+ * @param {Boolean} showTime If <code>true</code> The playback time position
26
+ * is shown next to the playhead.
27
+ */
28
+
29
+ function PlayheadLayer(peaks, view, segmentsGroup, showTime) {
30
+ this._peaks = peaks;
31
+ this._view = view;
32
+ this._segmentsGroup = segmentsGroup;
33
+ this._playheadPixel = 0;
34
+ this._playheadLineAnimation = null;
35
+ this._playheadVisible = false;
36
+ this._playheadColor = peaks.options.playheadColor;
37
+ this._playheadTextColor = peaks.options.playheadTextColor;
38
+
39
+ this._playheadLayer = new Konva.Layer();
40
+
41
+ this._activeSegment = null;
42
+ this._lastActiveSegmentId = null;
43
+
44
+ this._createPlayhead(this._playheadColor);
45
+
46
+ if (showTime) {
47
+ this._createPlayheadText(this._playheadTextColor);
48
+ }
49
+
50
+ this.fitToView();
51
+
52
+ this.zoomLevelChanged();
53
+
54
+ this._peaks.on('segments.remove_all', this._onSegmentsRemoveAll.bind(this));
55
+ this._peaks.on('segments.remove', this._onSegmentsRemove.bind(this));
56
+ }
57
+
58
+ PlayheadLayer.prototype._onSegmentsRemoveAll = function() {
59
+ this._activeSegment = null;
60
+ this._lastActiveSegmentId = null;
61
+ };
62
+
63
+ PlayheadLayer.prototype._onSegmentsRemove = function(segments) {
64
+ if (this._activeSegment || this._lastActiveSegmentId) {
65
+ var activeSegmentId = this._activeSegment ? this._activeSegment.id : null;
66
+ var lastActiveSegmentId = this._lastActiveSegmentId ? this._lastActiveSegmentId.id : null;
67
+
68
+ for (var id in segments) {
69
+ if (Utils.objectHasProperty(segments, id)) {
70
+ if (segments[id].id === activeSegmentId) {
71
+ this._activeSegment = null;
72
+ }
73
+ if (segments[id].id === lastActiveSegmentId) {
74
+ this._lastActiveSegmentId = null;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Adds the layer to the given {Konva.Stage}.
83
+ *
84
+ * @param {Konva.Stage} stage
85
+ */
86
+
87
+ PlayheadLayer.prototype.addToStage = function(stage) {
88
+ stage.add(this._playheadLayer);
89
+ };
90
+
91
+ /**
92
+ * Decides whether to use an animation to update the playhead position.
93
+ *
94
+ * If the zoom level is such that the number of pixels per second of audio is
95
+ * low, we can use timeupdate events from the HTMLMediaElement to
96
+ * set the playhead position. Otherwise, we use an animation to update the
97
+ * playhead position more smoothly. The animation is CPU intensive, so we
98
+ * avoid using it where possible.
99
+ */
100
+
101
+ PlayheadLayer.prototype.zoomLevelChanged = function() {
102
+ var pixelsPerSecond = this._view.timeToPixels(1.0);
103
+ var time;
104
+
105
+ this._useAnimation = pixelsPerSecond >= 5;
106
+
107
+ if (this._useAnimation) {
108
+ if (this._peaks.player.isPlaying() && !this._playheadLineAnimation) {
109
+ // Start the animation
110
+ this._start();
111
+ }
112
+ }
113
+ else {
114
+ if (this._playheadLineAnimation) {
115
+ // Stop the animation
116
+ time = this._peaks.player.getCurrentTime();
117
+
118
+ this.stop(time);
119
+ }
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Resizes the playhead UI objects to fit the available space in the
125
+ * view.
126
+ */
127
+
128
+ PlayheadLayer.prototype.fitToView = function() {
129
+ var height = this._view.getHeight();
130
+
131
+ this._playheadLine.points([0.5, 0, 0.5, height]);
132
+
133
+ // if (this._playheadText) {
134
+ // this._playheadText.y(30);
135
+ // }
136
+ };
137
+
138
+ /**
139
+ * Creates the playhead UI objects.
140
+ *
141
+ * @private
142
+ * @param {String} color
143
+ */
144
+
145
+ PlayheadLayer.prototype._createPlayhead = function(color) {
146
+ // Create with default points, the real values are set in fitToView().
147
+ this._playheadLine = new Konva.Line({
148
+ stroke: color,
149
+ strokeWidth: 3
150
+ });
151
+
152
+ this._playheadHandle = new Konva.RegularPolygon({
153
+ x: 0.5,
154
+ y: HANDLE_RADIUS / 2,
155
+ sides: 3,
156
+ fill: color,
157
+ strokeWidth: 0,
158
+ radius: HANDLE_RADIUS,
159
+ rotation: 180
160
+ });
161
+
162
+ var self = this;
163
+
164
+ this._playheadGroup = new Konva.Group({
165
+ x: 0,
166
+ y: 0,
167
+ draggable: true,
168
+ dragBoundFunc: function(pos) {
169
+ var time = Math.max(
170
+ 0,
171
+ self._view.pixelsToTime(
172
+ pos.x + self._view.getFrameOffset()
173
+ )
174
+ );
175
+
176
+ var autoPos = self._view.updateWithAutoScroll(
177
+ this,
178
+ function() {
179
+ time = Math.max(
180
+ 0,
181
+ self._view.pixelsToTime(
182
+ self._view.getPointerPosition().x + self._view.getFrameOffset()
183
+ )
184
+ );
185
+
186
+ self._peaks.player.seek(time);
187
+ },
188
+ function() {
189
+ self._peaks.player.seek(time);
190
+ }
191
+ );
192
+
193
+ return {
194
+ x: autoPos.x,
195
+ y: autoPos.y
196
+ };
197
+ }
198
+ });
199
+
200
+ this._playheadGroup.on('dragstart', this._onPlayheadDragStart.bind(this));
201
+ this._playheadGroup.on('dragend', this._onPlayheadDragEnd.bind(this));
202
+
203
+ this._playheadGroup.add(this._playheadHandle);
204
+ this._playheadGroup.add(this._playheadLine);
205
+ this._playheadLayer.add(this._playheadGroup);
206
+ };
207
+
208
+ PlayheadLayer.prototype._onPlayheadDragStart = function() {
209
+ this._view.enableAutoScroll(false);
210
+ this._dragging = true;
211
+ this._scrollInterval = null;
212
+ };
213
+
214
+ PlayheadLayer.prototype._onPlayheadDragEnd = function() {
215
+ this._view.enableAutoScroll(true);
216
+ this._dragging = false;
217
+ if (this._playheadGroup._scrollingInterval) {
218
+ clearInterval(this._playheadGroup._scrollingInterval);
219
+ this._playheadGroup._scrollingInterval = null;
220
+ }
221
+ };
222
+
223
+ PlayheadLayer.prototype._createPlayheadText = function(color) {
224
+ // Create with default y, the real value is set in fitToView().
225
+ this._playheadText = new Konva.Text({
226
+ x: 13,
227
+ y: 3,
228
+ text: '00:00:00',
229
+ fontSize: 11,
230
+ fontFamily: 'sans-serif',
231
+ fill: color,
232
+ align: 'right',
233
+ listening: false
234
+ });
235
+
236
+ this._playheadGroup.add(this._playheadText);
237
+ };
238
+
239
+ /**
240
+ * Updates the playhead position.
241
+ *
242
+ * @param {Number} time Current playhead position, in seconds.
243
+ */
244
+
245
+ PlayheadLayer.prototype.updatePlayheadTime = function(time) {
246
+ this._syncPlayhead(time);
247
+
248
+ if (this._peaks.player.isPlaying()) {
249
+ this._start();
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Updates the playhead position.
255
+ *
256
+ * @private
257
+ * @param {Number} time Current playhead position, in seconds.
258
+ */
259
+
260
+ PlayheadLayer.prototype._syncPlayhead = function(time) {
261
+ var pixelIndex = this._view.timeToPixels(time);
262
+
263
+ var frameOffset = this._view.timeToPixels(this._view.getTimeOffset());
264
+ var width = this._view.getWidth();
265
+
266
+ var isVisible = (pixelIndex + HANDLE_RADIUS >= frameOffset) &&
267
+ (pixelIndex - HANDLE_RADIUS < frameOffset + width);
268
+
269
+ this._playheadPixel = pixelIndex;
270
+
271
+ if (isVisible) {
272
+ var playheadX = this._playheadPixel - frameOffset;
273
+
274
+ if (!this._playheadVisible) {
275
+ this._playheadVisible = true;
276
+ this._playheadGroup.show();
277
+ }
278
+
279
+ var playheadPositionDiff = this._playheadGroup.x() - playheadX;
280
+
281
+ if (playheadPositionDiff) {
282
+ var newActiveSegment = this._segmentsGroup.getActiveSegment(
283
+ this._view.pixelsToTime(playheadX + frameOffset),
284
+ null,
285
+ true
286
+ );
287
+
288
+ if (newActiveSegment !== this._activeSegment) {
289
+ if (this._activeSegment) {
290
+ this._peaks.emit('segments.exit', this._activeSegment);
291
+ this._activeSegment = null;
292
+ }
293
+ if (newActiveSegment) {
294
+ this._peaks.emit('segments.enter', newActiveSegment);
295
+ this._activeSegment = newActiveSegment;
296
+ this._lastActiveSegment = this._activeSegment;
297
+ }
298
+ }
299
+ }
300
+
301
+ this._playheadGroup.setAttr('x', playheadX);
302
+
303
+ if (this._playheadText) {
304
+ var text = Utils.formatTime(time, false);
305
+
306
+ this._playheadText.setText(text);
307
+
308
+ this._peaks.emit(
309
+ 'playhead.moved',
310
+ this._playheadText.getAbsolutePosition().x,
311
+ this._playheadText.width()
312
+ );
313
+ }
314
+
315
+ this._playheadLayer.draw();
316
+ }
317
+ else {
318
+ if (this._playheadVisible) {
319
+ this._playheadVisible = false;
320
+ this._playheadGroup.hide();
321
+
322
+ this._playheadLayer.draw();
323
+
324
+ this._peaks.emit(
325
+ 'playhead.hidden'
326
+ );
327
+ }
328
+ }
329
+ };
330
+
331
+ /**
332
+ * Starts a playhead animation in sync with the media playback.
333
+ *
334
+ * @private
335
+ */
336
+
337
+ PlayheadLayer.prototype._start = function() {
338
+ var self = this;
339
+
340
+ if (self._playheadLineAnimation) {
341
+ self._playheadLineAnimation.stop();
342
+ self._playheadLineAnimation = null;
343
+ }
344
+
345
+ if (!self._useAnimation) {
346
+ return;
347
+ }
348
+
349
+ var lastPlayheadPosition = null;
350
+
351
+ self._playheadLineAnimation = new Konva.Animation(function() {
352
+ var time = self._peaks.player.getCurrentTime();
353
+ var playheadPosition = self._view.timeToPixels(time);
354
+
355
+ if (playheadPosition !== lastPlayheadPosition) {
356
+ self._syncPlayhead(time);
357
+ lastPlayheadPosition = playheadPosition;
358
+ }
359
+ }, self._playheadLayer);
360
+
361
+ self._playheadLineAnimation.start();
362
+ };
363
+
364
+ PlayheadLayer.prototype.stop = function(time) {
365
+ if (this._playheadLineAnimation) {
366
+ this._playheadLineAnimation.stop();
367
+ this._playheadLineAnimation = null;
368
+ }
369
+
370
+ this._syncPlayhead(time);
371
+ };
372
+
373
+ /**
374
+ * Returns the position of the playhead marker, in pixels relative to the
375
+ * left hand side of the waveform view.
376
+ *
377
+ * @return {Number}
378
+ */
379
+
380
+ PlayheadLayer.prototype.getPlayheadOffset = function() {
381
+ return this._playheadPixel - this._view.getFrameOffset();
382
+ };
383
+
384
+ PlayheadLayer.prototype.getPlayheadPixel = function() {
385
+ return this._playheadPixel;
386
+ };
387
+
388
+ PlayheadLayer.prototype.showPlayheadTime = function(show) {
389
+ var updated = false;
390
+
391
+ if (show) {
392
+ if (!this._playheadText) {
393
+ // Create it
394
+ this._createPlayheadText(this._playheadTextColor);
395
+ updated = true;
396
+ }
397
+ }
398
+ else {
399
+ if (this._playheadText) {
400
+ this._playheadText.remove();
401
+ this._playheadText.destroy();
402
+ this._playheadText = null;
403
+ updated = true;
404
+ }
405
+ }
406
+
407
+ if (updated) {
408
+ this._playheadLayer.draw();
409
+ }
410
+ };
411
+
412
+ return PlayheadLayer;
413
+ });