tencent.jquery.pix.component 1.0.77 → 1.0.78

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.
@@ -1,758 +1,767 @@
1
- import { $, windowEnv } from "../config";
2
- import './videocss.scss';
3
- import VideoHTML from "./videohtml";
4
-
5
- /**
6
- * @typedef {"play" | "pause" | "stop" | "enterFullScreen" | "exitFullScreen"} VideoPlayerState
7
- * 视频播放器状态
8
- */
9
-
10
- /**
11
- * 在页面上放置一个视频播放器组件(腾讯视频)
12
- * @constructor
13
- * @param {object} options 选项
14
- * @param {string} options.container 容器元素的jquery选择器
15
- * @param {string} options.vid 腾讯视频id
16
- * @param {string} [options.videoUrl] 视频地址,非腾讯视频时使用,比如上传到cos的视频
17
- * @param {string} [options.previewUrl] 视频预览图
18
- * @param {string} [options.timeUnit] 视频的时间单位 默认是s秒 ,如果是ms,则传1000
19
- * @param {0|1} [options.videoType=1] 视频类型选项,0: 标清,1: 高清
20
- * @param {number} [options.autoHideControlsDelayMs=5000] 自动隐藏控制条延迟时间,单位ms
21
- * @param {boolean} [options.showProgressBar=true] 是否显示进度条
22
- * @param {boolean} [options.showVolumeControl=false] 是否显示音量控制
23
- * @param {boolean} [options.clickToPause=false] 点击播放区域是否直接暂停,true: 直接暂停,false: 先展示控制条
24
- * @param {(this: VideoPlayer, type: VideoPlayerState) => any} [options.stateChanged] 状态变化回调
25
- */
26
- export function VideoPlayer(options) {
27
- this.options = options
28
- if (typeof this.options.stateChanged !== 'function') {
29
- this.options.stateChanged = () => { };
30
- }
31
- this.init()
32
- }
33
-
34
- VideoPlayer.prototype.init = async function () {
35
- if ('timeUnit' in this.options) {
36
- // 浏览器,单位s ; PixUI,单位ms
37
- this.timeUnit = Number(this.options.timeUnit) || 1;
38
- } else {
39
- if (windowEnv === 'pix') {
40
- this.timeUnit = 1000;
41
- } else {
42
- this.timeUnit = 1;
43
- }
44
- }
45
-
46
-
47
-
48
- const $container = $(this.options.container);
49
- this.$container = $container;
50
- $container.html(VideoHTML);
51
-
52
- if (this.options.previewUrl) {
53
- $container.find('.myplayer-video-preview').css('background-image', `url(${this.options.previewUrl})`);
54
- }
55
-
56
- // 缺省选项
57
- this.options = {
58
- videoType: 1,
59
- autoHideControlsDelayMs: 5000,
60
- showProgressBar: true,
61
- showVolumeControl: false,
62
- clickToPause: false,
63
- ...this.options
64
- }
65
- //如果vid不存在,则直接获取option.videoUrl
66
- const videoURL = this.options.vid ? await getVideoURL(this.options.vid, this.options.videoType) : this.options.videoUrl;
67
- //如果videoUrl不存在,则抛出错误
68
- if (!videoURL) {
69
-
70
- throw new Error('videoURL is required');
71
-
72
- }
73
- $container.find('.myplayer-video-mask').before(`<video src="${videoURL}"></video>`);
74
-
75
- this.state = {
76
- playing: false,
77
- controlsVisible: false,
78
- updatingProgress: false,
79
- controlsDelayStart: 0,
80
- firstPlay: true,
81
- isFullScreen: false,
82
- originalParent: null, // 保存原始父容器引用
83
- originalNextSibling: null, // 保存原始位置的下一个兄弟节点
84
- }
85
-
86
- // 根据showProgressBar选项控制进度条显示
87
- if (!this.options.showProgressBar) {
88
- $container.find('.myplayer-progress').hide();
89
- }
90
-
91
- // 根据showVolumeControl选项控制音量控件显示
92
- if (this.options.showVolumeControl) {
93
- $container.find('.myplayer-volume').show();
94
- // 初始化音量状态
95
- this.state.volume = 50;
96
- this.state.volumePanelVisible = false;
97
- this.updateVolumeDisplay(50);
98
- } else {
99
- $container.find('.myplayer-volume').hide();
100
- }
101
-
102
- this.bindEvent();
103
- }
104
-
105
- VideoPlayer.prototype.bindEvent = function () {
106
- const container = this.$container;
107
- container.off()
108
- .on('click', '.myplayer-video-cover', () => {
109
- this.play();
110
- })
111
- .on('click', '.myplayer-video-mask', () => {
112
- this.maskClicked();
113
- })
114
- .on('click', '.myplayer-btn-play', () => {
115
- this.play();
116
- })
117
- .on('click', '.myplayer-btn-pause', () => {
118
- this.pause();
119
- })
120
- .on('click', '.myplayer-btn-full-screen', () => {
121
- this.toggleFullScreen();
122
- })
123
- .on('click', '.myplayer-btn-exit-full-screen', () => {
124
- this.toggleFullScreen();
125
- })
126
- .on('canplay', 'video', () => {
127
- console.log('canplay');
128
- this.updateTotalTime();
129
- })
130
- .on('loadedmetadata', 'video', () => {
131
- console.log('loadedmetadata');
132
- this.updateTotalTime();
133
- })
134
- .on('durationchange', 'video', () => {
135
- console.log('durationchange');
136
- this.updateTotalTime();
137
- })
138
- .on('ended', 'video', () => {
139
- console.log('video ended');
140
- this.stop();
141
- });
142
-
143
- // 只有在显示进度条时才绑定进度条相关事件
144
- if (this.options.showProgressBar) {
145
- container
146
- .on('dragstart', () => { console.log('video container dragstart') })
147
- .on('drag', () => { console.log('video container drag') })
148
- .on('dragend', () => { console.log('video container dragend') })
149
- .on('click', () => { console.log('video container click') })
150
- .on('dragstart', '.myplayer-progress', (e) => {
151
- if (e.originalEvent) {
152
- this.progressDragStart(e.originalEvent);
153
- } else {
154
- this.progressDragStart(e);
155
- }
156
- })
157
- .on('drag', '.myplayer-progress', (e) => {
158
- if (e.originalEvent) {
159
- this.progressDrag(e.originalEvent);
160
- } else {
161
- this.progressDrag(e);
162
- }
163
- })
164
- .on('dragend', '.myplayer-progress', (e) => {
165
- if (e.originalEvent) {
166
- this.progressDragEnd(e.originalEvent);
167
- } else {
168
- this.progressDragEnd(e);
169
- }
170
- })
171
- .on('click', '.myplayer-progress', (e) => {
172
- if (e.originalEvent) {
173
- this.progressDragEnd(e.originalEvent);
174
- } else {
175
- this.progressDragEnd(e);
176
- }
177
- });
178
- }
179
-
180
- // 只有在显示音量控件时才绑定音量相关事件
181
- if (this.options.showVolumeControl) {
182
- container
183
- .on('click', '.myplayer-volume-icon', () => {
184
- this.toggleVolumePanel();
185
- })
186
- .on('click', '.myplayer-volume-off-icon', () => {
187
- this.toggleVolumePanel();
188
- })
189
- .on('drag', '.myplayer-volume-line', (e) => {
190
- if (e.originalEvent) {
191
- this.volumeDrag(e.originalEvent);
192
- } else {
193
- this.volumeDrag(e);
194
- }
195
- })
196
- .on('dragend', '.myplayer-volume-line', (e) => {
197
- if (e.originalEvent) {
198
- this.volumeDragEnd(e.originalEvent);
199
- } else {
200
- this.volumeDragEnd(e);
201
- }
202
- })
203
- .on('click', '.myplayer-volume-line', (e) => {
204
- if (e.originalEvent) {
205
- this.volumeDragEnd(e.originalEvent);
206
- } else {
207
- this.volumeDragEnd(e);
208
- }
209
- });
210
- }
211
- }
212
-
213
- VideoPlayer.prototype.play = function () {
214
- if (this.state.firstPlay) {
215
- this.state.firstPlay = false;
216
- this.$container.find('.myplayer-video-cover,.myplayer-video-preview').hide();
217
- this.updateTotalTime();
218
- }
219
- const video = this.$container.find('video')[0];
220
- video.play();
221
- this.state.playing = true;
222
- if (this.options.showVolumeControl) {
223
- const volume = Math.round(video.volume * 100);
224
- this.state.volume = volume;
225
- this.updateVolumeDisplay(volume);
226
- }
227
- this.$container.find('.myplayer-btn-play').hide();
228
- this.$container.find('.myplayer-btn-big-play').addClass('myplayer-transparent');
229
- this.$container.find('.myplayer-btn-pause').show();
230
- this.$container.find('.myplayer-progress').removeClass('myplayer-progress-bold');
231
- this.startUpdatingProgress();
232
- this.hideControlsWithDelay();
233
- this.options.stateChanged.call(this, 'play');
234
- }
235
-
236
- VideoPlayer.prototype.pause = function () {
237
- this.showControls();
238
- const video = this.$container.find('video')[0];
239
- video.pause();
240
- this.state.playing = false;
241
- this.$container.find('.myplayer-btn-play').show();
242
- this.$container.find('.myplayer-btn-big-play').removeClass('myplayer-transparent');
243
- this.$container.find('.myplayer-btn-pause').hide();
244
- this.$container.find('.myplayer-progress').addClass('myplayer-progress-bold');
245
- this.state.controlsDelayStart = Date.now();
246
- this.options.stateChanged.call(this, 'pause');
247
- }
248
-
249
- VideoPlayer.prototype.stop = function () {
250
-
251
- const video = this.$container.find('video')[0];
252
- video.pause();
253
- video.currentTime = 0;
254
- this.state.playing = false;
255
- this.$container.find('.myplayer-btn-play').show();
256
- this.$container.find('.myplayer-btn-big-play').removeClass('myplayer-transparent');
257
- this.$container.find('.myplayer-btn-pause').hide();
258
- this.$container.find('.myplayer-progress').addClass('myplayer-progress-bold');
259
- this.state.controlsDelayStart = Date.now();
260
- this.$container.find('.myplayer-video-cover,.myplayer-video-preview').show();
261
- this.state.firstPlay = true;
262
- this.options.stateChanged.call(this, 'stop');
263
- }
264
-
265
- VideoPlayer.prototype.maskClicked = function () {
266
- if (this.state.volumePanelVisible) {
267
- this.toggleVolumePanel();
268
- return;
269
- }
270
-
271
- if (this.state.controlsVisible === true) {
272
- if (this.state.playing === true) {
273
- this.pause();
274
- } else {
275
- this.play();
276
- }
277
- } else {
278
- if (this.options.clickToPause && this.state.playing === true) {
279
- // 如果配置了点击直接暂停,且正在播放,则直接暂停
280
- this.pause();
281
- } else {
282
- // 否则先展示控制条
283
- this.showControls();
284
- }
285
- }
286
- }
287
-
288
- VideoPlayer.prototype.showControls = function () {
289
- const container = this.$container;
290
- container.find('.myplayer-video-mask').removeClass('myplayer-transparent');
291
- container.find('.myplayer-top').removeClass('myplayer-transparent');
292
- container.find('.myplayer-bottom').removeClass('myplayer-transparent');
293
- this.state.controlsVisible = true;
294
- this.state.updatingProgress = true;
295
- this.updateTotalTime();
296
- this.startUpdatingProgress();
297
-
298
- this.hideControlsWithDelay();
299
- }
300
-
301
- VideoPlayer.prototype.hideControls = function () {
302
- const container = this.$container;
303
- container.find('.myplayer-video-mask').addClass('myplayer-transparent');
304
- container.find('.myplayer-top').addClass('myplayer-transparent');
305
- container.find('.myplayer-bottom').addClass('myplayer-transparent');
306
- this.state.controlsVisible = false;
307
- this.state.updatingProgress = false;
308
- }
309
-
310
- VideoPlayer.prototype.hideControlsWithDelay = function () {
311
- this.state.controlsDelayStart = Date.now();
312
- setTimeout(() => {
313
- const nowTime = Date.now();
314
- // console.debug("nowTime - this.state.controlsDelayStart", nowTime - this.state.controlsDelayStart);
315
-
316
- // 有时正常时间差也会小于设定的时间,所以判断时减去100ms
317
- if (!this.state.volumePanelVisible && this.state.playing === true && nowTime - this.state.controlsDelayStart >= this.options.autoHideControlsDelayMs - 100) {
318
- this.hideControls();
319
- }
320
- }, this.options.autoHideControlsDelayMs);
321
- }
322
-
323
- VideoPlayer.prototype.startUpdatingProgress = function () {
324
- this.updateProgress();
325
-
326
- if (this.state.updatingProgress) {
327
- window.requestAnimationFrame(this.startUpdatingProgress.bind(this));
328
- }
329
- }
330
-
331
- VideoPlayer.prototype.updateProgress = function () {
332
- const video = this.$container.find('video')[0];
333
- if (!video) return;
334
-
335
- const currentTime = video.currentTime || 0;
336
- const duration = video.duration || 0;
337
-
338
- // 防止duration为NaN或0时计算出错
339
- if (duration && !isNaN(duration) && duration > 0) {
340
- const progress = currentTime / duration;
341
- const parentWidth = this.$container.find('.myplayer-progress').width();
342
- this.$container.find('.myplayer-subprogress').width(`${progress * parentWidth}px`);
343
- //当progress为1时,将暂停按钮隐藏,播放按钮显示
344
- if (progress >= 1) {
345
- this.$container.find('.myplayer-btn-pause').hide();
346
- this.$container.find('.myplayer-btn-play').show();
347
- }
348
- }
349
-
350
- // 格式化当前播放时间(currentTime单位为秒)
351
- this.$container.find('.myplayer-playtime').html(this.formatTime(currentTime));
352
- }
353
-
354
- VideoPlayer.prototype.updateTotalTime = function () {
355
- const video = this.$container.find('video')[0];
356
- if (!video) return;
357
- const duration = video.duration;
358
-
359
- // 只有当duration有效时才更新显示
360
- if (duration && !isNaN(duration) && isFinite(duration) && duration > 0) {
361
- // duration单位为秒
362
- this.$container.find('.myplayer-totaltime').html(this.formatTime(duration));
363
- }
364
- }
365
-
366
- VideoPlayer.prototype.toggleFullScreen = function () {
367
- const $container = this.$container;
368
- const fullScreenBtn = $container.find('.myplayer-btn-full-screen');
369
- const exitFullScreenBtn = $container.find('.myplayer-btn-exit-full-screen');
370
- const bottom = $container.find('.myplayer-bottom');
371
- const innerContainer = $container.find('.myplayer-container');
372
-
373
- if (innerContainer.hasClass('myplayer-full-screen')) {
374
- // 退出全屏:将容器移回原位置
375
- this._moveBackToOriginal();
376
- innerContainer.removeClass('myplayer-full-screen');
377
- bottom.removeClass('myplayer-full-screen');
378
- fullScreenBtn.show();
379
- exitFullScreenBtn.hide();
380
- this.state.isFullScreen = false;
381
- this.options.stateChanged.call(this, 'exitFullScreen');
382
- } else {
383
- // 进入全屏:将容器移动到body
384
- this._moveToBody();
385
- innerContainer.addClass('myplayer-full-screen');
386
- bottom.addClass('myplayer-full-screen');
387
- fullScreenBtn.hide();
388
- exitFullScreenBtn.show();
389
- this.state.isFullScreen = true;
390
- this.options.stateChanged.call(this, 'enterFullScreen');
391
- }
392
-
393
- this.hideControlsWithDelay();
394
- }
395
-
396
- VideoPlayer.prototype.setFullScreen = function (target = true) {
397
- const $container = this.$container;
398
- const fullScreenBtn = $container.find('.myplayer-btn-full-screen');
399
- const exitFullScreenBtn = $container.find('.myplayer-btn-exit-full-screen');
400
- const bottom = $container.find('.myplayer-bottom');
401
- const innerContainer = $container.find('.myplayer-container');
402
-
403
- if (!target && innerContainer.hasClass('myplayer-full-screen')) {
404
- // 退出全屏:将容器移回原位置
405
- this._moveBackToOriginal();
406
- innerContainer.removeClass('myplayer-full-screen');
407
- bottom.removeClass('myplayer-full-screen');
408
- fullScreenBtn.show();
409
- exitFullScreenBtn.hide();
410
- this.state.isFullScreen = false;
411
- this.options.stateChanged.call(this, 'exitFullScreen');
412
- } else if (target && !innerContainer.hasClass('myplayer-full-screen')) {
413
- // 进入全屏:将容器移动到body
414
- this._moveToBody();
415
- innerContainer.addClass('myplayer-full-screen');
416
- bottom.addClass('myplayer-full-screen');
417
- fullScreenBtn.hide();
418
- exitFullScreenBtn.show();
419
- this.state.isFullScreen = true;
420
- this.options.stateChanged.call(this, 'enterFullScreen');
421
- }
422
-
423
- this.hideControlsWithDelay();
424
- }
425
-
426
- /**
427
- * 将容器移动到body(进入全屏时调用)
428
- * @private
429
- */
430
- VideoPlayer.prototype._moveToBody = function () {
431
- const containerEl = this.$container[0];
432
-
433
- // 保存原始位置信息(仅在第一次移动时保存)
434
- if (!this.state.originalParent) {
435
- this.state.originalParent = containerEl.parentNode;
436
- this.state.originalNextSibling = containerEl.nextSibling;
437
- }
438
-
439
- // 从原容器解绑
440
- this.$container.off();
441
- // 从body解绑所有相关事件
442
- $(document.body).off('click.myplayer');
443
- $(document.body).off('canplay.myplayer');
444
- $(document.body).off('dragstart.myplayer');
445
- $(document.body).off('drag.myplayer');
446
- $(document.body).off('dragend.myplayer');
447
-
448
- // 移动到body
449
- document.body.appendChild(containerEl);
450
-
451
- // 重新绑定事件
452
- this.bindEvent();
453
-
454
- const video = this.$container.find('video')[0];
455
- if (this.state.playing && typeof video?.play === 'function') {
456
- video.play(); // 部分PixUI运行环境在元素移动后,video元素会自动暂停,需要手动播放
457
- }
458
- }
459
-
460
- /**
461
- * 将容器移回原位置(退出全屏时调用)
462
- * @private
463
- */
464
- VideoPlayer.prototype._moveBackToOriginal = function () {
465
- const containerEl = this.$container[0];
466
-
467
- // 如果没有保存原始位置,说明还没移动过,直接返回
468
- if (!this.state.originalParent) {
469
- return;
470
- }
471
-
472
- // 解绑事件
473
- this.$container.off();
474
-
475
- // 移回原位置
476
- if (this.state.originalNextSibling) {
477
- this.state.originalParent.insertBefore(containerEl, this.state.originalNextSibling);
478
- } else {
479
- this.state.originalParent.appendChild(containerEl);
480
- }
481
-
482
- // 重新绑定事件
483
- this.bindEvent();
484
-
485
- const video = this.$container.find('video')[0];
486
- if (this.state.playing && typeof video?.play === 'function') {
487
- video.play(); // 部分PixUI运行环境在元素移动后,video元素会自动暂停,需要手动播放
488
- }
489
- }
490
-
491
- /**
492
- * 进度条拖动开始
493
- * @param {MouseEvent} e
494
- */
495
- VideoPlayer.prototype.progressDragStart = function (e) {
496
- if (!this.options.showProgressBar) {
497
- return;
498
- }
499
-
500
- this.state.updatingProgress = false;
501
- if (this.state.playing === true) {
502
- this.pause();
503
- }
504
- }
505
-
506
- /**
507
- * 进度条拖动
508
- * @param {MouseEvent} e
509
- */
510
- VideoPlayer.prototype.progressDrag = function (e) {
511
- if (!this.options.showProgressBar) {
512
- return;
513
- }
514
-
515
- const $container = this.$container;
516
- const progress = $container.find('.myplayer-progress');
517
- const subprogress = $container.find('.myplayer-subprogress');
518
- const video = $container.find('video')[0];
519
-
520
- const parentWidth = progress.width();
521
- // 使用getBoundingClientRect计算准确的点击位置
522
- const rect = progress[0].getBoundingClientRect();
523
- const clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
524
- const curOffset = Math.max(Math.min(clientX - rect.left, parentWidth), 0);
525
-
526
- subprogress.width(`${curOffset}px`);
527
- const adjustedPlayTime = curOffset / parentWidth * video.duration;
528
- $container.find('.myplayer-playtime').html(this.formatTime(adjustedPlayTime));
529
- }
530
-
531
- /**
532
- * 进度条拖动结束
533
- * @param {MouseEvent} e
534
- */
535
- VideoPlayer.prototype.progressDragEnd = function (e) {
536
- if (!this.options.showProgressBar) {
537
- return;
538
- }
539
-
540
- const $container = this.$container;
541
- const video = $container.find('video')[0];
542
- const progress = $container.find('.myplayer-progress');
543
- const parentWidth = progress.width();
544
-
545
- // 使用getBoundingClientRect计算准确的点击位置
546
- const rect = progress[0].getBoundingClientRect();
547
- const clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
548
- const curOffset = Math.max(Math.min(clientX - rect.left, parentWidth), 0);
549
-
550
- const duration = video.duration;//this.getCurrentDuration();
551
- // 计算目标播放时间(单位:秒)
552
- const targetTime = (curOffset / parentWidth) * duration;
553
-
554
- // 确保duration有效再设置currentTime
555
- if (duration && !isNaN(duration) && duration > 0) {
556
-
557
- // 这里的currentTime单位为秒
558
- video.currentTime = this.getCurrentDuration(targetTime);
559
- }
560
-
561
- this.play();
562
-
563
- this.state.updatingProgress = true;
564
- this.startUpdatingProgress();
565
- }
566
-
567
- // 格式化时间
568
- VideoPlayer.prototype.formatTime = function (duration) {
569
- let secs = duration;
570
- if (this.timeUnit !== 1) {
571
- secs = secs / this.timeUnit;
572
- }
573
- const minutes = Math.floor(secs / 60);
574
- const seconds = Math.floor(secs % 60);
575
- return `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
576
- }
577
-
578
- // 获取当前的视频时间
579
- VideoPlayer.prototype.getCurrentDuration = function (duration) {
580
- let secs = duration;
581
- if (this.timeUnit !== 1) {
582
- secs = secs / this.timeUnit;
583
- }
584
- return secs;
585
- }
586
-
587
- /**
588
- * 切换音量面板显示/隐藏
589
- */
590
- VideoPlayer.prototype.toggleVolumePanel = function () {
591
- if (!this.options.showVolumeControl) {
592
- return;
593
- }
594
-
595
- const $volumeRate = this.$container.find('.myplayer-volume-rate');
596
- if (this.state.volumePanelVisible) {
597
- $volumeRate.hide();
598
- this.state.volumePanelVisible = false;
599
- this.hideControlsWithDelay();
600
- } else {
601
- $volumeRate.show();
602
- this.state.volumePanelVisible = true;
603
- }
604
- }
605
-
606
- /**
607
- * 更新音量显示
608
- * @param {number} volume - 音量值 0-100
609
- */
610
- VideoPlayer.prototype.updateVolumeDisplay = function (volume) {
611
- const $container = this.$container;
612
- $container.find('.myplayer-volume-rate text').text(Math.round(volume));
613
- $container.find('.myplayer-volume-subline').css('height', `${volume}%`);
614
-
615
- // 根据音量更新图标显示
616
- if (volume === 0) {
617
- $container.find('.myplayer-volume-icon').hide();
618
- $container.find('.myplayer-volume-off-icon').show();
619
- } else {
620
- $container.find('.myplayer-volume-icon').show();
621
- $container.find('.myplayer-volume-off-icon').hide();
622
- }
623
- }
624
-
625
- /**
626
- * 设置视频音量
627
- * @param {number} volume - 音量值 0-100
628
- */
629
- VideoPlayer.prototype.setVolume = function (volume) {
630
- const video = this.$container.find('video')[0];
631
- if (!video) return;
632
-
633
- // 限制音量范围 0-100
634
- volume = Math.max(0, Math.min(100, volume));
635
- this.state.volume = volume;
636
-
637
- // video.volume 范围是 0-1
638
- video.volume = volume / 100;
639
- video.muted = volume === 0;
640
-
641
- this.updateVolumeDisplay(volume);
642
- }
643
-
644
- /**
645
- * 音量条拖动中
646
- * @param {MouseEvent} e
647
- */
648
- VideoPlayer.prototype.volumeDrag = function (e) {
649
- this.setVolumeByMouseEvent(e);
650
- }
651
-
652
- /**
653
- * 音量条拖动结束
654
- * @param {MouseEvent} e
655
- */
656
- VideoPlayer.prototype.volumeDragEnd = function (e) {
657
- this.setVolumeByMouseEvent(e);
658
- }
659
-
660
- VideoPlayer.prototype.setVolumeByMouseEvent = function (e) {
661
- if (!this.options.showVolumeControl) {
662
- return;
663
- }
664
-
665
- const $container = this.$container;
666
- const volumeLine = $container.find('.myplayer-volume-line');
667
-
668
- const parentHeight = volumeLine.height();
669
- // 使用getBoundingClientRect计算准确的点击位置
670
- const rect = volumeLine[0].getBoundingClientRect();
671
- const clientY = e.clientY != undefined ? e.clientY : (e.touches ? e.touches[0].clientY : 0);
672
- // 音量条是从下往上增长的,所以需要反转计算
673
- const curOffset = Math.max(Math.min(rect.bottom - clientY, parentHeight), 0);
674
-
675
- const volume = (curOffset / parentHeight) * 100;
676
- if (!isFinite(volume)) {
677
- return;
678
- }
679
- this.updateVolumeDisplay(volume);
680
- this.setVolume(volume);
681
- }
682
-
683
- /**
684
- * 清理VideoPlayer实例,移除所有事件监听器、定时器、DOM元素等
685
- */
686
- VideoPlayer.prototype.remove = function () {
687
- // 停止视频播放
688
- this.stop();
689
-
690
- // 停止所有定时器和动画帧
691
- this.state.updatingProgress = false;
692
-
693
- // 如果处于全屏状态,先退出全屏
694
- if (this.state.isFullScreen) {
695
- this._moveBackToOriginal();
696
- }
697
-
698
- // 移除所有事件监听器
699
- this.$container.off();
700
-
701
- // 移除document.body上的相关事件监听器
702
- $(document.body).off('click.myplayer');
703
- $(document.body).off('canplay.myplayer');
704
- $(document.body).off('dragstart.myplayer');
705
- $(document.body).off('drag.myplayer');
706
- $(document.body).off('dragend.myplayer');
707
-
708
- // 清理容器内容
709
- this.$container.empty();
710
- }
711
-
712
- /**
713
- * 获取视频真实地址
714
- * @param {string} vid 腾讯视频vid
715
- * @param {0|1} [type] 类型,0: 标清,1: 高清
716
- * @returns {Promise<string>} 真实视频地址
717
- */
718
- const getVideoURL = async function (vid, type = 1) {
719
- const sApiUrl = `https://vv.video.qq.com/getinfo?vid=${vid}&platform=101001&charge=0&otype=json&t=${Date.now()}`;
720
-
721
- try {
722
-
723
- const res = await fetchData(sApiUrl);
724
-
725
- const processedStr = res.replace('QZOutputJson=', '');
726
- const jsonStr = processedStr.substring(0, processedStr.length - 1);
727
- const resObj = JSON.parse(jsonStr);
728
- const sRealUrl = `${resObj.vl.vi[0].ul.ui[type].url}${resObj.vl.vi[0].fn}?vkey=${resObj.vl.vi[0].fvkey}`;
729
-
730
- return sRealUrl;
731
- } catch (err) {
732
- console.error(err)
733
- return '';
734
- }
735
- }
736
-
737
- /**
738
- * 异步get请求
739
- * @param {string} url
740
- * @returns {Promise<string>}
741
- */
742
- const fetchData = function (url) {
743
- const xhr = new XMLHttpRequest();
744
- xhr.open('GET', url);
745
- xhr.send();
746
-
747
- return new Promise((resolve, reject) => {
748
- xhr.onreadystatechange = function () {
749
- if (xhr.readyState === 4) {
750
- if (xhr.status === 200) {
751
- resolve(xhr.responseText); // 请求成功,返回响应内容
752
- } else {
753
- reject(new Error('请求失败')); // 请求失败,返回错误对象
754
- }
755
- }
756
- };
757
- });
758
- }
1
+ import { $, windowEnv } from "../config";
2
+ import './videocss.scss';
3
+ import VideoHTML from "./videohtml";
4
+
5
+ /**
6
+ * @typedef {"play" | "pause" | "stop" | "enterFullScreen" | "exitFullScreen"} VideoPlayerState
7
+ * 视频播放器状态
8
+ */
9
+
10
+ /**
11
+ * 在页面上放置一个视频播放器组件(腾讯视频)
12
+ * @constructor
13
+ * @param {object} options 选项
14
+ * @param {string} options.container 容器元素的jquery选择器
15
+ * @param {string} options.vid 腾讯视频id
16
+ * @param {string} [options.videoUrl] 视频地址,非腾讯视频时使用,比如上传到cos的视频
17
+ * @param {string} [options.previewUrl] 视频预览图
18
+ * @param {string} [options.timeUnit] 视频的时间单位 默认是s秒 ,如果是ms,则传1000
19
+ * @param {0|1} [options.videoType=1] 视频类型选项,0: 标清,1: 高清
20
+ * @param {number} [options.autoHideControlsDelayMs=5000] 自动隐藏控制条延迟时间,单位ms
21
+ * @param {boolean} [options.showProgressBar=true] 是否显示进度条
22
+ * @param {boolean} [options.showVolumeControl=false] 是否显示音量控制
23
+ * @param {boolean} [options.showProgressHandle=true] 是否显示进度条滑块
24
+ * @param {boolean} [options.clickToPause=false] 点击播放区域是否直接暂停,true: 直接暂停,false: 先展示控制条
25
+ * @param {(this: VideoPlayer, type: VideoPlayerState) => any} [options.stateChanged] 状态变化回调
26
+ */
27
+ export function VideoPlayer(options) {
28
+ this.options = options
29
+ if (typeof this.options.stateChanged !== 'function') {
30
+ this.options.stateChanged = () => { };
31
+ }
32
+ this.init()
33
+ }
34
+
35
+ VideoPlayer.prototype.init = async function () {
36
+ if ('timeUnit' in this.options) {
37
+ // 浏览器,单位s ; PixUI,单位ms
38
+ this.timeUnit = Number(this.options.timeUnit) || 1;
39
+ } else {
40
+ if (windowEnv === 'pix') {
41
+ this.timeUnit = 1000;
42
+ } else {
43
+ this.timeUnit = 1;
44
+ }
45
+ }
46
+
47
+
48
+
49
+ const $container = $(this.options.container);
50
+ this.$container = $container;
51
+ $container.html(VideoHTML);
52
+
53
+ if (this.options.previewUrl) {
54
+ $container.find('.myplayer-video-preview').css('background-image', `url(${this.options.previewUrl})`);
55
+ }
56
+
57
+ // 缺省选项
58
+ this.options = {
59
+ videoType: 1,
60
+ autoHideControlsDelayMs: 5000,
61
+ showProgressBar: true,
62
+ showVolumeControl: false,
63
+ showProgressHandle: true,
64
+ clickToPause: false,
65
+ ...this.options
66
+ }
67
+ //如果vid不存在,则直接获取option.videoUrl
68
+ const videoURL = this.options.vid ? await getVideoURL(this.options.vid, this.options.videoType) : this.options.videoUrl;
69
+ //如果videoUrl不存在,则抛出错误
70
+ if (!videoURL) {
71
+
72
+ throw new Error('videoURL is required');
73
+
74
+ }
75
+ $container.find('.myplayer-video-mask').before(`<video src="${videoURL}"></video>`);
76
+
77
+ this.state = {
78
+ playing: false,
79
+ controlsVisible: false,
80
+ updatingProgress: false,
81
+ controlsDelayStart: 0,
82
+ firstPlay: true,
83
+ isFullScreen: false,
84
+ originalParent: null, // 保存原始父容器引用
85
+ originalNextSibling: null, // 保存原始位置的下一个兄弟节点
86
+ }
87
+
88
+ // 根据showProgressBar选项控制进度条显示
89
+ if (!this.options.showProgressBar) {
90
+ $container.find('.myplayer-progress').hide();
91
+ }
92
+
93
+ // 根据showProgressHandle选项控制进度条滑块显示
94
+ if (!this.options.showProgressHandle) {
95
+ $container.find('.myplayer-progress-handle').hide();
96
+ }
97
+
98
+ // 根据showVolumeControl选项控制音量控件显示
99
+ if (this.options.showVolumeControl) {
100
+ $container.find('.myplayer-volume').show();
101
+ // 初始化音量状态
102
+ this.state.volume = 50;
103
+ this.state.volumePanelVisible = false;
104
+ this.updateVolumeDisplay(50);
105
+ } else {
106
+ $container.find('.myplayer-volume').hide();
107
+ }
108
+
109
+ this.bindEvent();
110
+ }
111
+
112
+ VideoPlayer.prototype.bindEvent = function () {
113
+ const container = this.$container;
114
+ container.off()
115
+ .on('click', '.myplayer-video-cover', () => {
116
+ this.play();
117
+ })
118
+ .on('click', '.myplayer-video-mask', () => {
119
+ this.maskClicked();
120
+ })
121
+ .on('click', '.myplayer-btn-play', () => {
122
+ this.play();
123
+ })
124
+ .on('click', '.myplayer-btn-pause', () => {
125
+ this.pause();
126
+ })
127
+ .on('click', '.myplayer-btn-full-screen', () => {
128
+ this.toggleFullScreen();
129
+ })
130
+ .on('click', '.myplayer-btn-exit-full-screen', () => {
131
+ this.toggleFullScreen();
132
+ })
133
+ .on('canplay', 'video', () => {
134
+ console.log('canplay');
135
+ this.updateTotalTime();
136
+ })
137
+ .on('loadedmetadata', 'video', () => {
138
+ console.log('loadedmetadata');
139
+ this.updateTotalTime();
140
+ })
141
+ .on('durationchange', 'video', () => {
142
+ console.log('durationchange');
143
+ this.updateTotalTime();
144
+ })
145
+ .on('ended', 'video', () => {
146
+ console.log('video ended');
147
+ this.stop();
148
+ });
149
+
150
+ // 只有在显示进度条时才绑定进度条相关事件
151
+ if (this.options.showProgressBar) {
152
+ container
153
+ .on('dragstart', () => { console.log('video container dragstart') })
154
+ .on('drag', () => { console.log('video container drag') })
155
+ .on('dragend', () => { console.log('video container dragend') })
156
+ .on('click', () => { console.log('video container click') })
157
+ .on('dragstart', '.myplayer-progress', (e) => {
158
+ if (e.originalEvent) {
159
+ this.progressDragStart(e.originalEvent);
160
+ } else {
161
+ this.progressDragStart(e);
162
+ }
163
+ })
164
+ .on('drag', '.myplayer-progress', (e) => {
165
+ if (e.originalEvent) {
166
+ this.progressDrag(e.originalEvent);
167
+ } else {
168
+ this.progressDrag(e);
169
+ }
170
+ })
171
+ .on('dragend', '.myplayer-progress', (e) => {
172
+ if (e.originalEvent) {
173
+ this.progressDragEnd(e.originalEvent);
174
+ } else {
175
+ this.progressDragEnd(e);
176
+ }
177
+ })
178
+ .on('click', '.myplayer-progress', (e) => {
179
+ if (e.originalEvent) {
180
+ this.progressDragEnd(e.originalEvent);
181
+ } else {
182
+ this.progressDragEnd(e);
183
+ }
184
+ });
185
+ }
186
+
187
+ // 只有在显示音量控件时才绑定音量相关事件
188
+ if (this.options.showVolumeControl) {
189
+ container
190
+ .on('click', '.myplayer-volume-icon', () => {
191
+ this.toggleVolumePanel();
192
+ })
193
+ .on('click', '.myplayer-volume-off-icon', () => {
194
+ this.toggleVolumePanel();
195
+ })
196
+ .on('drag', '.myplayer-volume-line', (e) => {
197
+ if (e.originalEvent) {
198
+ this.volumeDrag(e.originalEvent);
199
+ } else {
200
+ this.volumeDrag(e);
201
+ }
202
+ })
203
+ .on('dragend', '.myplayer-volume-line', (e) => {
204
+ if (e.originalEvent) {
205
+ this.volumeDragEnd(e.originalEvent);
206
+ } else {
207
+ this.volumeDragEnd(e);
208
+ }
209
+ })
210
+ .on('click', '.myplayer-volume-line', (e) => {
211
+ if (e.originalEvent) {
212
+ this.volumeDragEnd(e.originalEvent);
213
+ } else {
214
+ this.volumeDragEnd(e);
215
+ }
216
+ });
217
+ }
218
+ }
219
+
220
+ VideoPlayer.prototype.play = function () {
221
+ if (this.state.firstPlay) {
222
+ this.state.firstPlay = false;
223
+ this.$container.find('.myplayer-video-cover,.myplayer-video-preview').hide();
224
+ this.updateTotalTime();
225
+ }
226
+ const video = this.$container.find('video')[0];
227
+ video.play();
228
+ this.state.playing = true;
229
+ if (this.options.showVolumeControl) {
230
+ const volume = Math.round(video.volume * 100);
231
+ this.state.volume = volume;
232
+ this.updateVolumeDisplay(volume);
233
+ }
234
+ this.$container.find('.myplayer-btn-play').hide();
235
+ this.$container.find('.myplayer-btn-big-play').addClass('myplayer-transparent');
236
+ this.$container.find('.myplayer-btn-pause').show();
237
+ this.$container.find('.myplayer-progress').removeClass('myplayer-progress-bold');
238
+ this.startUpdatingProgress();
239
+ this.hideControlsWithDelay();
240
+ this.options.stateChanged.call(this, 'play');
241
+ }
242
+
243
+ VideoPlayer.prototype.pause = function () {
244
+ this.showControls();
245
+ const video = this.$container.find('video')[0];
246
+ video.pause();
247
+ this.state.playing = false;
248
+ this.$container.find('.myplayer-btn-play').show();
249
+ this.$container.find('.myplayer-btn-big-play').removeClass('myplayer-transparent');
250
+ this.$container.find('.myplayer-btn-pause').hide();
251
+ this.$container.find('.myplayer-progress').addClass('myplayer-progress-bold');
252
+ this.state.controlsDelayStart = Date.now();
253
+ this.options.stateChanged.call(this, 'pause');
254
+ }
255
+
256
+ VideoPlayer.prototype.stop = function () {
257
+
258
+ const video = this.$container.find('video')[0];
259
+ video.pause();
260
+ video.currentTime = 0;
261
+ this.state.playing = false;
262
+ this.$container.find('.myplayer-btn-play').show();
263
+ this.$container.find('.myplayer-btn-big-play').removeClass('myplayer-transparent');
264
+ this.$container.find('.myplayer-btn-pause').hide();
265
+ this.$container.find('.myplayer-progress').addClass('myplayer-progress-bold');
266
+ this.state.controlsDelayStart = Date.now();
267
+ this.$container.find('.myplayer-video-cover,.myplayer-video-preview').show();
268
+ this.state.firstPlay = true;
269
+ this.options.stateChanged.call(this, 'stop');
270
+ }
271
+
272
+ VideoPlayer.prototype.maskClicked = function () {
273
+ if (this.state.volumePanelVisible) {
274
+ this.toggleVolumePanel();
275
+ return;
276
+ }
277
+
278
+ if (this.state.controlsVisible === true) {
279
+ if (this.state.playing === true) {
280
+ this.pause();
281
+ } else {
282
+ this.play();
283
+ }
284
+ } else {
285
+ if (this.options.clickToPause && this.state.playing === true) {
286
+ // 如果配置了点击直接暂停,且正在播放,则直接暂停
287
+ this.pause();
288
+ } else {
289
+ // 否则先展示控制条
290
+ this.showControls();
291
+ }
292
+ }
293
+ }
294
+
295
+ VideoPlayer.prototype.showControls = function () {
296
+ const container = this.$container;
297
+ container.find('.myplayer-video-mask').removeClass('myplayer-transparent');
298
+ container.find('.myplayer-top').removeClass('myplayer-transparent');
299
+ container.find('.myplayer-bottom').removeClass('myplayer-transparent');
300
+ this.state.controlsVisible = true;
301
+ this.state.updatingProgress = true;
302
+ this.updateTotalTime();
303
+ this.startUpdatingProgress();
304
+
305
+ this.hideControlsWithDelay();
306
+ }
307
+
308
+ VideoPlayer.prototype.hideControls = function () {
309
+ const container = this.$container;
310
+ container.find('.myplayer-video-mask').addClass('myplayer-transparent');
311
+ container.find('.myplayer-top').addClass('myplayer-transparent');
312
+ container.find('.myplayer-bottom').addClass('myplayer-transparent');
313
+ this.state.controlsVisible = false;
314
+ this.state.updatingProgress = false;
315
+ }
316
+
317
+ VideoPlayer.prototype.hideControlsWithDelay = function () {
318
+ this.state.controlsDelayStart = Date.now();
319
+ setTimeout(() => {
320
+ const nowTime = Date.now();
321
+ // console.debug("nowTime - this.state.controlsDelayStart", nowTime - this.state.controlsDelayStart);
322
+
323
+ // 有时正常时间差也会小于设定的时间,所以判断时减去100ms
324
+ if (!this.state.volumePanelVisible && this.state.playing === true && nowTime - this.state.controlsDelayStart >= this.options.autoHideControlsDelayMs - 100) {
325
+ this.hideControls();
326
+ }
327
+ }, this.options.autoHideControlsDelayMs);
328
+ }
329
+
330
+ VideoPlayer.prototype.startUpdatingProgress = function () {
331
+ this.updateProgress();
332
+
333
+ if (this.state.updatingProgress) {
334
+ window.requestAnimationFrame(this.startUpdatingProgress.bind(this));
335
+ }
336
+ }
337
+
338
+ VideoPlayer.prototype.updateProgress = function () {
339
+ const video = this.$container.find('video')[0];
340
+ if (!video) return;
341
+
342
+ const currentTime = video.currentTime || 0;
343
+ const duration = video.duration || 0;
344
+
345
+ // 防止duration为NaN或0时计算出错
346
+ if (duration && !isNaN(duration) && duration > 0) {
347
+ const progress = currentTime / duration;
348
+ const parentWidth = this.$container.find('.myplayer-progress').width();
349
+ this.$container.find('.myplayer-subprogress').width(`${progress * parentWidth}px`);
350
+ //当progress为1时,将暂停按钮隐藏,播放按钮显示
351
+ if (progress >= 1) {
352
+ this.$container.find('.myplayer-btn-pause').hide();
353
+ this.$container.find('.myplayer-btn-play').show();
354
+ }
355
+ }
356
+
357
+ // 格式化当前播放时间(currentTime单位为秒)
358
+ this.$container.find('.myplayer-playtime').html(this.formatTime(currentTime));
359
+ }
360
+
361
+ VideoPlayer.prototype.updateTotalTime = function () {
362
+ const video = this.$container.find('video')[0];
363
+ if (!video) return;
364
+ const duration = video.duration;
365
+
366
+ // 只有当duration有效时才更新显示
367
+ if (duration && !isNaN(duration) && isFinite(duration) && duration > 0) {
368
+ // duration单位为秒
369
+ this.$container.find('.myplayer-totaltime').html(this.formatTime(duration));
370
+ }
371
+ }
372
+
373
+ VideoPlayer.prototype.toggleFullScreen = function () {
374
+ const $container = this.$container;
375
+ const fullScreenBtn = $container.find('.myplayer-btn-full-screen');
376
+ const exitFullScreenBtn = $container.find('.myplayer-btn-exit-full-screen');
377
+ const bottom = $container.find('.myplayer-bottom');
378
+ const innerContainer = $container.find('.myplayer-container');
379
+
380
+ if (innerContainer.hasClass('myplayer-full-screen')) {
381
+ // 退出全屏:将容器移回原位置
382
+ this._moveBackToOriginal();
383
+ innerContainer.removeClass('myplayer-full-screen');
384
+ bottom.removeClass('myplayer-full-screen');
385
+ fullScreenBtn.show();
386
+ exitFullScreenBtn.hide();
387
+ this.state.isFullScreen = false;
388
+ this.options.stateChanged.call(this, 'exitFullScreen');
389
+ } else {
390
+ // 进入全屏:将容器移动到body
391
+ this._moveToBody();
392
+ innerContainer.addClass('myplayer-full-screen');
393
+ bottom.addClass('myplayer-full-screen');
394
+ fullScreenBtn.hide();
395
+ exitFullScreenBtn.show();
396
+ this.state.isFullScreen = true;
397
+ this.options.stateChanged.call(this, 'enterFullScreen');
398
+ }
399
+
400
+ this.hideControlsWithDelay();
401
+ }
402
+
403
+ VideoPlayer.prototype.setFullScreen = function (target = true) {
404
+ const $container = this.$container;
405
+ const fullScreenBtn = $container.find('.myplayer-btn-full-screen');
406
+ const exitFullScreenBtn = $container.find('.myplayer-btn-exit-full-screen');
407
+ const bottom = $container.find('.myplayer-bottom');
408
+ const innerContainer = $container.find('.myplayer-container');
409
+
410
+ if (!target && innerContainer.hasClass('myplayer-full-screen')) {
411
+ // 退出全屏:将容器移回原位置
412
+ this._moveBackToOriginal();
413
+ innerContainer.removeClass('myplayer-full-screen');
414
+ bottom.removeClass('myplayer-full-screen');
415
+ fullScreenBtn.show();
416
+ exitFullScreenBtn.hide();
417
+ this.state.isFullScreen = false;
418
+ this.options.stateChanged.call(this, 'exitFullScreen');
419
+ } else if (target && !innerContainer.hasClass('myplayer-full-screen')) {
420
+ // 进入全屏:将容器移动到body
421
+ this._moveToBody();
422
+ innerContainer.addClass('myplayer-full-screen');
423
+ bottom.addClass('myplayer-full-screen');
424
+ fullScreenBtn.hide();
425
+ exitFullScreenBtn.show();
426
+ this.state.isFullScreen = true;
427
+ this.options.stateChanged.call(this, 'enterFullScreen');
428
+ }
429
+
430
+ this.hideControlsWithDelay();
431
+ }
432
+
433
+ /**
434
+ * 将容器移动到body(进入全屏时调用)
435
+ * @private
436
+ */
437
+ VideoPlayer.prototype._moveToBody = function () {
438
+ const containerEl = this.$container[0];
439
+
440
+ // 保存原始位置信息(仅在第一次移动时保存)
441
+ if (!this.state.originalParent) {
442
+ this.state.originalParent = containerEl.parentNode;
443
+ this.state.originalNextSibling = containerEl.nextSibling;
444
+ }
445
+
446
+ // 从原容器解绑
447
+ this.$container.off();
448
+ // body解绑所有相关事件
449
+ $(document.body).off('click.myplayer');
450
+ $(document.body).off('canplay.myplayer');
451
+ $(document.body).off('dragstart.myplayer');
452
+ $(document.body).off('drag.myplayer');
453
+ $(document.body).off('dragend.myplayer');
454
+
455
+ // 移动到body
456
+ document.body.appendChild(containerEl);
457
+
458
+ // 重新绑定事件
459
+ this.bindEvent();
460
+
461
+ const video = this.$container.find('video')[0];
462
+ if (this.state.playing && typeof video?.play === 'function') {
463
+ video.play(); // 部分PixUI运行环境在元素移动后,video元素会自动暂停,需要手动播放
464
+ }
465
+ }
466
+
467
+ /**
468
+ * 将容器移回原位置(退出全屏时调用)
469
+ * @private
470
+ */
471
+ VideoPlayer.prototype._moveBackToOriginal = function () {
472
+ const containerEl = this.$container[0];
473
+
474
+ // 如果没有保存原始位置,说明还没移动过,直接返回
475
+ if (!this.state.originalParent) {
476
+ return;
477
+ }
478
+
479
+ // 解绑事件
480
+ this.$container.off();
481
+
482
+ // 移回原位置
483
+ if (this.state.originalNextSibling) {
484
+ this.state.originalParent.insertBefore(containerEl, this.state.originalNextSibling);
485
+ } else {
486
+ this.state.originalParent.appendChild(containerEl);
487
+ }
488
+
489
+ // 重新绑定事件
490
+ this.bindEvent();
491
+
492
+ const video = this.$container.find('video')[0];
493
+ if (this.state.playing && typeof video?.play === 'function') {
494
+ video.play(); // 部分PixUI运行环境在元素移动后,video元素会自动暂停,需要手动播放
495
+ }
496
+ }
497
+
498
+ /**
499
+ * 进度条拖动开始
500
+ * @param {MouseEvent} e
501
+ */
502
+ VideoPlayer.prototype.progressDragStart = function (e) {
503
+ if (!this.options.showProgressBar) {
504
+ return;
505
+ }
506
+ if (this.state.playing === true) {
507
+ this.pause();
508
+ }
509
+
510
+ // 更新进度条时,不更新播放时间
511
+ // 这个赋值必须放在最后,因为pause会设置this.state.updatingProgress为true
512
+ this.state.updatingProgress = false;
513
+ }
514
+
515
+ /**
516
+ * 进度条拖动
517
+ * @param {MouseEvent} e
518
+ */
519
+ VideoPlayer.prototype.progressDrag = function (e) {
520
+ if (!this.options.showProgressBar) {
521
+ return;
522
+ }
523
+
524
+ const $container = this.$container;
525
+ const progress = $container.find('.myplayer-progress');
526
+ const subprogress = $container.find('.myplayer-subprogress');
527
+ const video = $container.find('video')[0];
528
+
529
+ const parentWidth = progress.width();
530
+ // 使用getBoundingClientRect计算准确的点击位置
531
+ const rect = progress[0].getBoundingClientRect();
532
+ const clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
533
+ const curOffset = Math.max(Math.min(clientX - rect.left, parentWidth), 0);
534
+
535
+ subprogress.width(`${curOffset}px`);
536
+ const adjustedPlayTime = curOffset / parentWidth * video.duration;
537
+ $container.find('.myplayer-playtime').html(this.formatTime(adjustedPlayTime));
538
+ }
539
+
540
+ /**
541
+ * 进度条拖动结束
542
+ * @param {MouseEvent} e
543
+ */
544
+ VideoPlayer.prototype.progressDragEnd = function (e) {
545
+ if (!this.options.showProgressBar) {
546
+ return;
547
+ }
548
+
549
+ const $container = this.$container;
550
+ const video = $container.find('video')[0];
551
+ const progress = $container.find('.myplayer-progress');
552
+ const parentWidth = progress.width();
553
+
554
+ // 使用getBoundingClientRect计算准确的点击位置
555
+ const rect = progress[0].getBoundingClientRect();
556
+ const clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
557
+ const curOffset = Math.max(Math.min(clientX - rect.left, parentWidth), 0);
558
+
559
+ const duration = video.duration;//this.getCurrentDuration();
560
+ // 计算目标播放时间(单位:秒)
561
+ const targetTime = (curOffset / parentWidth) * duration;
562
+
563
+ // 确保duration有效再设置currentTime
564
+ if (duration && !isNaN(duration) && duration > 0) {
565
+
566
+ // 这里的currentTime单位为秒
567
+ video.currentTime = this.getCurrentDuration(targetTime);
568
+ }
569
+
570
+ this.play();
571
+
572
+ this.state.updatingProgress = true;
573
+ this.startUpdatingProgress();
574
+ }
575
+
576
+ // 格式化时间
577
+ VideoPlayer.prototype.formatTime = function (duration) {
578
+ let secs = duration;
579
+ if (this.timeUnit !== 1) {
580
+ secs = secs / this.timeUnit;
581
+ }
582
+ const minutes = Math.floor(secs / 60);
583
+ const seconds = Math.floor(secs % 60);
584
+ return `${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
585
+ }
586
+
587
+ // 获取当前的视频时间
588
+ VideoPlayer.prototype.getCurrentDuration = function (duration) {
589
+ let secs = duration;
590
+ if (this.timeUnit !== 1) {
591
+ secs = secs / this.timeUnit;
592
+ }
593
+ return secs;
594
+ }
595
+
596
+ /**
597
+ * 切换音量面板显示/隐藏
598
+ */
599
+ VideoPlayer.prototype.toggleVolumePanel = function () {
600
+ if (!this.options.showVolumeControl) {
601
+ return;
602
+ }
603
+
604
+ const $volumeRate = this.$container.find('.myplayer-volume-rate');
605
+ if (this.state.volumePanelVisible) {
606
+ $volumeRate.hide();
607
+ this.state.volumePanelVisible = false;
608
+ this.hideControlsWithDelay();
609
+ } else {
610
+ $volumeRate.show();
611
+ this.state.volumePanelVisible = true;
612
+ }
613
+ }
614
+
615
+ /**
616
+ * 更新音量显示
617
+ * @param {number} volume - 音量值 0-100
618
+ */
619
+ VideoPlayer.prototype.updateVolumeDisplay = function (volume) {
620
+ const $container = this.$container;
621
+ $container.find('.myplayer-volume-rate text').text(Math.round(volume));
622
+ $container.find('.myplayer-volume-subline').css('height', `${volume}%`);
623
+
624
+ // 根据音量更新图标显示
625
+ if (volume === 0) {
626
+ $container.find('.myplayer-volume-icon').hide();
627
+ $container.find('.myplayer-volume-off-icon').show();
628
+ } else {
629
+ $container.find('.myplayer-volume-icon').show();
630
+ $container.find('.myplayer-volume-off-icon').hide();
631
+ }
632
+ }
633
+
634
+ /**
635
+ * 设置视频音量
636
+ * @param {number} volume - 音量值 0-100
637
+ */
638
+ VideoPlayer.prototype.setVolume = function (volume) {
639
+ const video = this.$container.find('video')[0];
640
+ if (!video) return;
641
+
642
+ // 限制音量范围 0-100
643
+ volume = Math.max(0, Math.min(100, volume));
644
+ this.state.volume = volume;
645
+
646
+ // video.volume 范围是 0-1
647
+ video.volume = volume / 100;
648
+ video.muted = volume === 0;
649
+
650
+ this.updateVolumeDisplay(volume);
651
+ }
652
+
653
+ /**
654
+ * 音量条拖动中
655
+ * @param {MouseEvent} e
656
+ */
657
+ VideoPlayer.prototype.volumeDrag = function (e) {
658
+ this.setVolumeByMouseEvent(e);
659
+ }
660
+
661
+ /**
662
+ * 音量条拖动结束
663
+ * @param {MouseEvent} e
664
+ */
665
+ VideoPlayer.prototype.volumeDragEnd = function (e) {
666
+ this.setVolumeByMouseEvent(e);
667
+ }
668
+
669
+ VideoPlayer.prototype.setVolumeByMouseEvent = function (e) {
670
+ if (!this.options.showVolumeControl) {
671
+ return;
672
+ }
673
+
674
+ const $container = this.$container;
675
+ const volumeLine = $container.find('.myplayer-volume-line');
676
+
677
+ const parentHeight = volumeLine.height();
678
+ // 使用getBoundingClientRect计算准确的点击位置
679
+ const rect = volumeLine[0].getBoundingClientRect();
680
+ const clientY = e.clientY != undefined ? e.clientY : (e.touches ? e.touches[0].clientY : 0);
681
+ // 音量条是从下往上增长的,所以需要反转计算
682
+ const curOffset = Math.max(Math.min(rect.bottom - clientY, parentHeight), 0);
683
+
684
+ const volume = (curOffset / parentHeight) * 100;
685
+ if (!isFinite(volume)) {
686
+ return;
687
+ }
688
+ this.updateVolumeDisplay(volume);
689
+ this.setVolume(volume);
690
+ }
691
+
692
+ /**
693
+ * 清理VideoPlayer实例,移除所有事件监听器、定时器、DOM元素等
694
+ */
695
+ VideoPlayer.prototype.remove = function () {
696
+ // 停止视频播放
697
+ this.stop();
698
+
699
+ // 停止所有定时器和动画帧
700
+ this.state.updatingProgress = false;
701
+
702
+ // 如果处于全屏状态,先退出全屏
703
+ if (this.state.isFullScreen) {
704
+ this._moveBackToOriginal();
705
+ }
706
+
707
+ // 移除所有事件监听器
708
+ this.$container.off();
709
+
710
+ // 移除document.body上的相关事件监听器
711
+ $(document.body).off('click.myplayer');
712
+ $(document.body).off('canplay.myplayer');
713
+ $(document.body).off('dragstart.myplayer');
714
+ $(document.body).off('drag.myplayer');
715
+ $(document.body).off('dragend.myplayer');
716
+
717
+ // 清理容器内容
718
+ this.$container.empty();
719
+ }
720
+
721
+ /**
722
+ * 获取视频真实地址
723
+ * @param {string} vid 腾讯视频vid
724
+ * @param {0|1} [type] 类型,0: 标清,1: 高清
725
+ * @returns {Promise<string>} 真实视频地址
726
+ */
727
+ const getVideoURL = async function (vid, type = 1) {
728
+ const sApiUrl = `https://vv.video.qq.com/getinfo?vid=${vid}&platform=101001&charge=0&otype=json&t=${Date.now()}`;
729
+
730
+ try {
731
+
732
+ const res = await fetchData(sApiUrl);
733
+
734
+ const processedStr = res.replace('QZOutputJson=', '');
735
+ const jsonStr = processedStr.substring(0, processedStr.length - 1);
736
+ const resObj = JSON.parse(jsonStr);
737
+ const sRealUrl = `${resObj.vl.vi[0].ul.ui[type].url}${resObj.vl.vi[0].fn}?vkey=${resObj.vl.vi[0].fvkey}`;
738
+
739
+ return sRealUrl;
740
+ } catch (err) {
741
+ console.error(err)
742
+ return '';
743
+ }
744
+ }
745
+
746
+ /**
747
+ * 异步get请求
748
+ * @param {string} url
749
+ * @returns {Promise<string>}
750
+ */
751
+ const fetchData = function (url) {
752
+ const xhr = new XMLHttpRequest();
753
+ xhr.open('GET', url);
754
+ xhr.send();
755
+
756
+ return new Promise((resolve, reject) => {
757
+ xhr.onreadystatechange = function () {
758
+ if (xhr.readyState === 4) {
759
+ if (xhr.status === 200) {
760
+ resolve(xhr.responseText); // 请求成功,返回响应内容
761
+ } else {
762
+ reject(new Error('请求失败')); // 请求失败,返回错误对象
763
+ }
764
+ }
765
+ };
766
+ });
767
+ }