-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmedia-control.js
More file actions
322 lines (265 loc) · 9.61 KB
/
media-control.js
File metadata and controls
322 lines (265 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// export { mediaControl };
var mediaControl = (() => {
// * ---------------------------------------------------------------- utils
/**
* @param {number} v
* @param {[number, number]} range
*/
const inRange = (v, [min, max]) => Math.min(Math.max(v, min), max);
/**
* 判断 node 是否匹配 selector 函数的查询结果
* @param {HTMLElement} node
* @param { () => HTMLElement | ArrayLike<HTMLElement> | void } selectorFn
*/
const isMatchSelectorFn = (node, selectorFn) => {
if (!node) return false;
const n = selectorFn();
if (!n) return false;
return (n instanceof HTMLElement ? [n] : Array.from(n)).some((e) => e === node);
};
/** @returns HTMLMediaElement[] */
// @ts-ignore
const getMediaAll = () => [...document.getElementsByTagName("video"), ...document.getElementsByTagName("audio")];
// * ---------------------------------------------------------------- 音量
/**
* 手动音量倍率放大,解决有些源声音太小的问题
* @param {HTMLMediaElement} media
* @param {number} ratio
*/
const setVolumnRatio = (media, ratio) => {
const setVolumnRatio = (audio, ratio) => {
var audioCtx = new window.AudioContext();
var source = audioCtx.createMediaElementSource(audio);
var gainNode = audioCtx.createGain();
gainNode.gain.value = ratio;
source.connect(gainNode);
gainNode.connect(audioCtx.destination);
};
let did = false;
media.addEventListener("play", () => {
if (did) return;
did = true;
setVolumnRatio(media, ratio);
});
};
// * ---------------------------------------------------------------- 播放 跳转
/**
* @param {HTMLMediaElement} media
*/
const togglePlay = (media) => {
media.paused ? media.play() : media.pause();
};
/**
* @param {HTMLMediaElement} media
* @param {number} percent // [0, 100]
*/
const setPlaybackJumpToPercent = (media, percent) => {
media.currentTime = media.duration * percent;
};
/**
* @param {HTMLMediaElement} media
* @param {number} delta
*/
const setPlaybackJumpBySec = (media, delta) => {
media.currentTime = inRange(media.currentTime + delta, [0, media.duration]);
};
// * ---------------------------------------------------------------- 循环播放
/**
* 切换循环,可以指定传参,也可以自动切换
* 当开启循环时,如果当前为播放结束,则视为要重新自动播放
* @param {HTMLMediaElement} media
* @param {boolean} [state]
*/
const setReplayLoop = (media, state) => {
const shouldLoop = state ?? !media.loop;
media.loop = shouldLoop;
if (shouldLoop && media.ended) media.play();
};
// * ---------------------------------------------------------------- 播放速度
/** default rated speed value */
let lastSpeed = 1.75;
/**
* 增减播放速度
* @param {HTMLMediaElement} media
* @param {number} delta
* @param {[number,number]} range
*/
const setPlaybackSpeedBy = (media, delta, range) => {
const nextSpeed = inRange(media.playbackRate + delta, range);
media.playbackRate = nextSpeed;
if (nextSpeed != 1) lastSpeed = nextSpeed;
};
/**
* 切换播放速度
* @param {HTMLMediaElement} media
*/
const togglePlaybackSpeed = (media) => {
const curSpeed = media.playbackRate;
media.playbackRate = curSpeed === 1 ? lastSpeed : 1;
};
// * ---------------------------------------------------------------- 自动暂停其他标签页和本页其他视频
/**
* 播放标识,由 页面加载时间(用来简单识别为不同的页面)、页面URL、媒体src 构成
* 对于页面中的媒体来说,通常(简单防碰撞)这些是能快速获取的、固定的值,组合成唯一值
*
* @param {HTMLMediaElement} media */
const getMediaIdentifier = (media) => {
return {
pageTime: performance.timeOrigin,
url: location.href,
mediaSrc: media.src,
};
};
/**
* @param {Object} a
* @param {Object} b
* just simple check code
*/
const isSameMediaIdentifier = (a, b) => a.pageTime === b.pageTime && a.url === b.url && a.mediaSrc === b.mediaSrc;
// [...Object.keys(a), ...Object.keys(b)].every((k) => a[k] === b[k]);
/**
* 仅当媒体开始播放后,才可能需要被暂停,那么
*
* 当播放后
* 判断受控的媒体元素并暂存
* 广播正在播放的媒体标识
*
* 当接收到媒体标识后
* 只暂停记录的受控媒体
*
* 用 Set + WeakRef 而不是 WeakSet 因为可以遍历
* @type {Set<WeakRef<HTMLMediaElement>>}
*/
const medias = new Set();
/** @type {WeakMap<HTMLMediaElement,boolean>} */
const mediasControlled = new WeakMap();
// * ----------------
/**
* 当有多个页面一起播放时,播放当前的媒体则自动暂停其他标签页的媒体
* BroadcastChannel 仅限同源,姑且够用
*
* 满足 selector 的视为主要受控媒体(可以用来略过一些背景图视频或小窗预览视频,他们播放时一般无关紧要)
* 当媒体播放时,进行受控标记,仅当受控媒体播放时,才暂停其他标签页的媒体
*
* @param { () => HTMLMediaElement | ArrayLike<HTMLMediaElement> | void } [selectorFn]
*/
const enableGlobalSoloPlaying = (selectorFn = getMediaAll) => {
const pauseOthersBy = (mediaIds) => {
medias.forEach((e) => {
const media = e.deref();
if (!media) return medias.delete(e);
if (media.paused) return;
/** 或许由于 BroadcastChannel的机制,这个不可能会相同,不过还是检查一下以防万一 */
if (!isSameMediaIdentifier(mediaIds, getMediaIdentifier(media))) {
media.pause();
}
});
};
const bc = new BroadcastChannel("media-control");
bc.addEventListener("message", ({ data }) => pauseOthersBy(data));
/** @param {Event} e */
const handler = (e) => {
const media = e.target;
if (!(media instanceof HTMLMediaElement)) return;
/** 判断媒体是否需要受控,性能优化:缓存计算结果 */
if (!mediasControlled.has(media)) {
const shouldControl = isMatchSelectorFn(media, selectorFn);
mediasControlled.set(media, shouldControl ? true : false);
medias.add(new WeakRef(media));
}
const shouldControl = mediasControlled.get(media);
if (!shouldControl) return;
/** 暂停其他标签页受控视频,暂停本页其他受控视频 */
const mediaIds = getMediaIdentifier(media);
bc.postMessage(mediaIds);
pauseOthersBy(mediaIds);
};
document.addEventListener("play", handler, true);
/** youtube initial autoplay won't trigger play event, fix for it */
const onceHandler = (e) => {
handler(e);
document.removeEventListener("playing", onceHandler, true);
};
document.addEventListener("playing", onceHandler, true);
/** disconnect fn */
return () => document.removeEventListener("play", handler);
};
// * ---------------------------------------------------------------- toast
let toastTick = setTimeout(() => {});
/** <container, toastEl> */
const toastEls = new WeakMap();
/**
* @param {HTMLElement} container
* @param {string} text
*/
const toast = (container, text) => {
// * ---------------- prepare element
if (!container) return null;
if (!toastEls.get(container)) {
const toastEl = document.createElement("div");
toastEl.classList.add("mc-toast"); // id for css binding
container.appendChild(toastEl);
toastEls.set(container, toastEl);
}
// * ---------------- action
const toastEl = toastEls.get(container);
toastEl.textContent = text;
toastEl.classList.add("shown");
clearTimeout(toastTick);
toastTick = setTimeout(() => toastEl?.classList.remove("shown"), 1000);
};
// * ---------------------------------------------------------------- sound beep
/**
* 简单的 beep 声,可以用来做操作提示
* @param {number} frequency 毫秒
* @param {number} duration 毫秒
*/
const SoundBeep = (frequency, duration) =>
new Promise((resolve) => {
const context = new AudioContext();
const oscillator = context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.value = frequency;
oscillator.connect(context.destination);
oscillator.start();
setTimeout(function () {
oscillator.stop();
resolve();
}, duration);
});
// * ---------------------------------------------------------------- 截图
/**
* @param {HTMLVideoElement} video
* @returns {Promise<void>}
*/
const videoSnap = (video) =>
new Promise((res) => {
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]).then(() => res());
});
});
// * ---------------------------------------------------------------- export
/**
* 播放列表控制每个网站都不一样,还有网页全屏之类的功能,用模拟点击网页按钮来实现
* 所以这里只封装对单个音视频的播放状态进行的控制
*/
return {
getMediaIdentifier,
isSameMediaIdentifier,
setVolumnRatio,
enableGlobalSoloPlaying,
togglePlay,
setPlaybackJumpToPercent,
setPlaybackJumpBySec,
setReplayLoop,
setPlaybackSpeedBy,
togglePlaybackSpeed,
toast,
SoundBeep,
videoSnap,
};
})();