@vnejs/plugins.core.media 0.0.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/const/const.js +9 -0
- package/const/events.js +9 -0
- package/index.js +11 -0
- package/modules/gif.js +30 -0
- package/modules/load.js +96 -0
- package/modules/media.js +45 -0
- package/modules/memory.js +46 -0
- package/modules/utils.js +2 -0
- package/package.json +22 -0
package/const/const.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const TYPES = { IMAGE: "image", AUDIO: "audio", VIDEO: "video" };
|
|
2
|
+
|
|
3
|
+
export const CONSTRUCTORS = {
|
|
4
|
+
[TYPES.IMAGE]: Image,
|
|
5
|
+
[TYPES.AUDIO]: Audio,
|
|
6
|
+
[TYPES.VIDEO]: HTMLVideoElement,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const PRIORITIES = { HIGH: 0, MEDIUM: 1, LOW: 2, BACKGROUND: 3 };
|
package/const/events.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { regPlugin } from "@vnejs/shared";
|
|
2
|
+
|
|
3
|
+
import * as constants from "./const/const";
|
|
4
|
+
import { SUBSCRIBE_EVENTS } from "./const/events";
|
|
5
|
+
|
|
6
|
+
import { MediaGif } from "./modules/gif";
|
|
7
|
+
import { MediaLoad } from "./modules/load";
|
|
8
|
+
import { Media } from "./modules/media";
|
|
9
|
+
import { MediaMemory } from "./modules/memory";
|
|
10
|
+
|
|
11
|
+
regPlugin("MEDIA", { constants, events: SUBSCRIBE_EVENTS }, [MediaGif, MediaLoad, Media, MediaMemory]);
|
package/modules/gif.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import decodeGif from "decode-gif";
|
|
2
|
+
|
|
3
|
+
import { Module } from "@vnejs/module";
|
|
4
|
+
|
|
5
|
+
export class MediaGif extends Module {
|
|
6
|
+
name = "media.gif";
|
|
7
|
+
|
|
8
|
+
subscribe = () => {
|
|
9
|
+
this.on(this.EVENTS.MEDIA.GIF, this.onMediaGif);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
onMediaGif = ({ media, mediaId, ab, isConstant, isTmp } = {}) => {
|
|
13
|
+
const { width, height, frames } = decodeGif(new Uint8Array(ab));
|
|
14
|
+
|
|
15
|
+
media.setAttribute("frames", frames.length);
|
|
16
|
+
media.setAttribute("delay", frames[1].timeCode);
|
|
17
|
+
|
|
18
|
+
return Promise.all(this.createFrameFunc(mediaId, width, height, isConstant, isTmp));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
createFrameFunc = (mediaId, width, height, isConstant, isTmp) => (frame, i) => {
|
|
22
|
+
const media = document.createElement("canvas");
|
|
23
|
+
media.width = width;
|
|
24
|
+
media.height = height;
|
|
25
|
+
media.getContext("2d").putImageData(new ImageData(frame.data, width, height), 0, 0);
|
|
26
|
+
media.setAttribute("parentId", mediaId);
|
|
27
|
+
|
|
28
|
+
return this.emit(this.EVENTS.MEDIA.SET, { media, mediaId: `${mediaId}.frame.${i}`, isConstant, isTmp });
|
|
29
|
+
};
|
|
30
|
+
}
|
package/modules/load.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Semaphore } from "@vnejs/semaphore";
|
|
2
|
+
import { Module } from "@vnejs/module";
|
|
3
|
+
|
|
4
|
+
import { createMediaId } from "./utils";
|
|
5
|
+
|
|
6
|
+
export class MediaLoad extends Module {
|
|
7
|
+
name = "media.load";
|
|
8
|
+
|
|
9
|
+
inProcess = {};
|
|
10
|
+
|
|
11
|
+
subscribe = () => {
|
|
12
|
+
this.on(this.EVENTS.MEDIA.LOAD, this.onMediaLoad);
|
|
13
|
+
this.on(this.EVENTS.MEDIA.LOAD_CHECK, this.onMediaLoadCheck);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
init = ({ platform = this.CONST.PLATFORMS.WEB } = {}) => {
|
|
17
|
+
this.semaphore = new Semaphore(platform === this.CONST.PLATFORMS.WEB ? 8 : 4);
|
|
18
|
+
this.shared.mediaNoTimeoutSources = platform === this.CONST.PLATFORMS.WEB ? ["web"] : [];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
onMediaLoad = async ({ type, name, isConstant = false, isTmp = false, ...otherArgs } = {}) => {
|
|
22
|
+
if (!this.isReady) await this.waitIsReady();
|
|
23
|
+
|
|
24
|
+
const { mediaType, postfix, quality = "original", priority = this.CONST.MEDIA.PRIORITIES.BACKGROUND } = otherArgs;
|
|
25
|
+
|
|
26
|
+
const src = this.media[mediaType] && this.media[mediaType][`${quality}/${name}`];
|
|
27
|
+
const mediaId = createMediaId(type, mediaType, quality, name, postfix);
|
|
28
|
+
|
|
29
|
+
if (!src) {
|
|
30
|
+
const errorArgs = [type, name, mediaType, `${quality}/${name}`, this.media[mediaType], mediaId, src];
|
|
31
|
+
console.error(`No media src on preload:`, ...errorArgs);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const oldMedia = await this.emitOne(this.EVENTS.MEDIA.LOAD_CHECK, { mediaId });
|
|
36
|
+
if (oldMedia) return oldMedia;
|
|
37
|
+
|
|
38
|
+
await this.semaphore.push(mediaId, priority, this.getPromise(mediaId, type, src, priority, isConstant, isTmp));
|
|
39
|
+
|
|
40
|
+
return this.emitOne(this.EVENTS.MEDIA.MEMORY_GET, { mediaId });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
onMediaLoadCheck = async (mediaId) => {
|
|
44
|
+
if (this.inProcess[mediaId]) return this.inProcess[mediaId];
|
|
45
|
+
|
|
46
|
+
const oldMedia = await this.emitOne(this.EVENTS.MEDIA.MEMORY_GET, { mediaId });
|
|
47
|
+
if (oldMedia) return oldMedia;
|
|
48
|
+
|
|
49
|
+
if (this.inProcess[mediaId]) return this.inProcess[mediaId];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
getPromise =
|
|
53
|
+
(mediaId, type, src, priority, isConstant = false, isTmp = false) =>
|
|
54
|
+
async () => {
|
|
55
|
+
const oldMedia = await this.emitOne(this.EVENTS.MEDIA.LOAD_CHECK, { mediaId });
|
|
56
|
+
if (oldMedia) return oldMedia;
|
|
57
|
+
|
|
58
|
+
this.inProcess[mediaId] = this.loadMedia(mediaId, type, src, priority, isConstant, isTmp);
|
|
59
|
+
const media = await this.inProcess[mediaId];
|
|
60
|
+
delete this.inProcess[mediaId];
|
|
61
|
+
|
|
62
|
+
return media;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
loadMedia = async (mediaId, type, src, priority, isConstant = false, isTmp = false) => {
|
|
66
|
+
this.log({ action: "load start", mediaId, type, priority, isConstant, isTmp });
|
|
67
|
+
|
|
68
|
+
const ts = Math.round(new Date().getTime() / 1000 / 60 / 60);
|
|
69
|
+
const mediaSrc = `${src}?ts=${ts}&version=${this.options?.global?.version || 0}`;
|
|
70
|
+
const splitSrc = src.split(".");
|
|
71
|
+
const ext = splitSrc[splitSrc.length - 1];
|
|
72
|
+
const mediaRequest = await fetch(mediaSrc);
|
|
73
|
+
const media =
|
|
74
|
+
type === this.CONST.MEDIA.TYPES.VIDEO
|
|
75
|
+
? document.createElement("video")
|
|
76
|
+
: new this.CONST.MEDIA.CONSTRUCTORS[type]();
|
|
77
|
+
media.setAttribute("ext", ext);
|
|
78
|
+
media.setAttribute("type", type);
|
|
79
|
+
// IOS - mediaSrc
|
|
80
|
+
media.src = URL.createObjectURL(await mediaRequest.clone().blob());
|
|
81
|
+
|
|
82
|
+
if (ext === "gif") {
|
|
83
|
+
const ab = await mediaRequest.clone().arrayBuffer();
|
|
84
|
+
await this.emit(this.EVENTS.MEDIA.GIF, { media, mediaId, ab, isConstant, isTmp });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await this.emit(this.EVENTS.MEDIA.SET, { media, mediaId, isConstant, isTmp });
|
|
88
|
+
|
|
89
|
+
if (priority && !this.shared.mediaNoTimeoutSources.length)
|
|
90
|
+
await this.waitTimeout(Math.round(25 * priority * (Math.random() + 0.618)));
|
|
91
|
+
|
|
92
|
+
this.log({ action: "load end", mediaId, type, priority, isConstant, isTmp });
|
|
93
|
+
|
|
94
|
+
return media;
|
|
95
|
+
};
|
|
96
|
+
}
|
package/modules/media.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Module } from "@vnejs/module";
|
|
2
|
+
|
|
3
|
+
import { createMediaId } from "./utils";
|
|
4
|
+
|
|
5
|
+
export class Media extends Module {
|
|
6
|
+
name = "media";
|
|
7
|
+
|
|
8
|
+
subscribe = () => {
|
|
9
|
+
this.on(this.EVENTS.MEDIA.GET, this.onMediaGet);
|
|
10
|
+
this.on(this.EVENTS.MEDIA.SET, this.onMediaSet);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
onMediaGet = async ({ type, name, mediaType, postfix, quality = "original" } = {}) => {
|
|
14
|
+
if (!this.isReady) await this.waitIsReady();
|
|
15
|
+
|
|
16
|
+
const realMediaId = type && name && mediaType && createMediaId(type, mediaType, quality, name, postfix);
|
|
17
|
+
|
|
18
|
+
if (!realMediaId) return null;
|
|
19
|
+
|
|
20
|
+
const media =
|
|
21
|
+
(await this.emitOne(this.EVENTS.MEDIA.MEMORY_GET, { mediaId: realMediaId })) ||
|
|
22
|
+
(await this.loadMedia(type, name, mediaType, quality, postfix));
|
|
23
|
+
|
|
24
|
+
return this.prepareMedia(media);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
onMediaSet = async ({ mediaId, media, isConstant, isTmp } = {}) =>
|
|
28
|
+
this.emit(this.EVENTS.MEDIA.MEMORY_SET, { mediaId, media, isConstant, isTmp });
|
|
29
|
+
|
|
30
|
+
loadMedia = (type, name, mediaType, quality, postfix) => {
|
|
31
|
+
const priority = this.CONST.MEDIA.PRIORITIES.HIGH;
|
|
32
|
+
|
|
33
|
+
return this.emitOne(this.EVENTS.MEDIA.LOAD, { type, name, mediaType, quality, postfix, priority });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
prepareMedia = async (media) => {
|
|
37
|
+
const mediaType = media.getAttribute("type");
|
|
38
|
+
|
|
39
|
+
if (mediaType === this.CONST.MEDIA.TYPES.AUDIO || mediaType === this.CONST.MEDIA.TYPES.VIDEO)
|
|
40
|
+
while (!media.duration || media.readyState !== 4) await this.waitRerender();
|
|
41
|
+
if (mediaType === this.CONST.MEDIA.TYPES.IMAGE) while (!media.width || !media.complete) await this.waitRerender();
|
|
42
|
+
|
|
43
|
+
return media;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Module } from "@vnejs/module";
|
|
2
|
+
|
|
3
|
+
export class MediaMemory extends Module {
|
|
4
|
+
name = "media.memory";
|
|
5
|
+
|
|
6
|
+
subscribe = () => {
|
|
7
|
+
this.on(this.EVENTS.MEDIA.MEMORY_GET, this.onMemoryGet);
|
|
8
|
+
this.on(this.EVENTS.MEDIA.MEMORY_SET, this.onMemorySet);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
init = () => {
|
|
12
|
+
this.elem = document.createElement("div");
|
|
13
|
+
this.elem.id = "media";
|
|
14
|
+
this.elem.style.display = "none";
|
|
15
|
+
this.elem.style.position = "fixed";
|
|
16
|
+
|
|
17
|
+
document.getElementById("app").appendChild(this.elem);
|
|
18
|
+
|
|
19
|
+
const eventArgs = { module: this.name, onClear: this.onClear, onAdd: this.onAdd, onRestore: this.onRestore };
|
|
20
|
+
|
|
21
|
+
return this.emit(this.EVENTS.MEMORY.REG, eventArgs);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
onAdd = async (key, value) => {
|
|
25
|
+
this.elem.appendChild(value);
|
|
26
|
+
value.id = key;
|
|
27
|
+
while (!document.getElementById(key)) await this.waitRerender();
|
|
28
|
+
await this.waitRerender();
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
onClear = (key, memory, func) => {
|
|
32
|
+
const value = document.getElementById(key);
|
|
33
|
+
value.src && URL.revokeObjectURL(value.src);
|
|
34
|
+
value.remove();
|
|
35
|
+
|
|
36
|
+
return Promise.all(this.getPromises(key, memory, func));
|
|
37
|
+
};
|
|
38
|
+
onRestore = (key, memory, func) => Promise.all(this.getPromises(key, memory, func));
|
|
39
|
+
|
|
40
|
+
onMemorySet = async ({ media, mediaId, isConstant, isTmp } = {}) =>
|
|
41
|
+
this.emit(this.EVENTS.MEMORY.SET, { module: this.name, key: mediaId, value: media, isConstant, isTmp });
|
|
42
|
+
onMemoryGet = ({ mediaId }) => this.emitOne(this.EVENTS.MEMORY.GET, { module: this.name, key: mediaId });
|
|
43
|
+
|
|
44
|
+
getPromises = (key, memory, func) =>
|
|
45
|
+
[...document.querySelectorAll(`[parentId="${key}"]`)].map((children) => func(memory, children.id));
|
|
46
|
+
}
|
package/modules/utils.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vnejs/plugins.core.media",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
+
"publish:major": "npm version major && npm publish --access public",
|
|
8
|
+
"publish:minor": "npm version minor && npm publish --access public",
|
|
9
|
+
"publish:patch": "npm version patch && npm publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"description": "",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"decode-gif": "1.0.1"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@vnejs/shared": "*",
|
|
19
|
+
"@vnejs/module": "*",
|
|
20
|
+
"@vnejs/semaphore": "*"
|
|
21
|
+
}
|
|
22
|
+
}
|