@leafer-in/video 0.0.0-stage → 2.3.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/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@leafer-in/video",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.3.1",
4
+ "description": "@leafer-in/video",
5
+ "author": "Chao (Leafer) Wan",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/video.esm.min.js",
9
+ "exports": {
10
+ "import": "./dist/video.esm.min.js",
11
+ "require": "./dist/video.min.cjs",
12
+ "types": "./types/index.d.ts"
13
+ },
14
+ "types": "types/index.d.ts",
15
+ "unpkg": "dist/video.min.js",
16
+ "jsdelivr": "dist/video.min.js",
17
+ "files": ["src","types","dist"],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/leaferjs/leafer-in.git"
21
+ },
22
+ "homepage": "https://github.com/leaferjs/leafer-in/tree/main/packages/video",
23
+ "bugs": "https://github.com/leaferjs/leafer-in/issues",
24
+ "keywords": [
25
+ "leafer video",
26
+ "leafer-video",
27
+ "leafer-in",
28
+ "video",
29
+ "leafer-ui",
30
+ "leaferjs"
31
+ ],
32
+ "peerDependencies": {
33
+ "@leafer-ui/draw": "^2.2.4",
34
+ "@leafer-ui/interface": "^2.2.4",
35
+ "@leafer-in/interface": "^2.2.4"
36
+ }
6
37
  }
@@ -0,0 +1,88 @@
1
+ import { ILeafPaint, IVideoDecoder, IUI } from '@leafer-ui/interface'
2
+ import { isArray } from '@leafer-ui/draw'
3
+
4
+ export class PaintVideo {
5
+
6
+ public element: IUI
7
+
8
+ public get playerOptions(): ILeafPaint { const { fill } = (this.element || this as any).__; return isArray(fill) && fill.find(item => item.type === 'video') }
9
+
10
+ public get decoder(): IVideoDecoder { const { playerOptions } = this; return playerOptions && playerOptions.image && playerOptions.image.view }
11
+
12
+ public get duration(): number { // 获取当前播放进度 (秒)
13
+ const video = this.decoder
14
+ return video && video.duration
15
+ }
16
+
17
+ public get currentTime(): number { // 获取当前播放进度 (秒)
18
+ const video = this.decoder
19
+ return video && video.currentTime
20
+ }
21
+
22
+ public set currentTime(time: number) { // 设置播放进度
23
+ const video = this.decoder
24
+ if (video) {
25
+ video.currentTime = time;
26
+ (this.element || this as any as IUI).forceRender()// 需要强制更新一下元素
27
+ }
28
+ }
29
+
30
+ public get volume(): number { // 获取当前音量 (0 ~ 1)
31
+ const video = this.decoder
32
+ return video && video.volume
33
+ }
34
+
35
+ public set volume(volume: number) { // 设置音量 (0 ~ 1)
36
+ const video = this.decoder
37
+ if (video) video.volume = volume
38
+ }
39
+
40
+ public get paused(): boolean {
41
+ const video = this.decoder
42
+ return video && video.paused
43
+ }
44
+
45
+ public get ended(): boolean {
46
+ const video = this.decoder
47
+ return video && video.ended
48
+ }
49
+
50
+
51
+ constructor(element: IUI) {
52
+ this.element = element
53
+ }
54
+
55
+
56
+ public togglePlay(): void {
57
+ const { playerOptions } = this
58
+ if (playerOptions) {
59
+ if (playerOptions.paused !== false) this.play()
60
+ else this.pause()
61
+ }
62
+ }
63
+
64
+ public play(): void {
65
+ const { playerOptions } = this
66
+ if (playerOptions && playerOptions.paused !== false) {
67
+ playerOptions.paused = false;
68
+ (this.element || this as any as IUI).forceRender()
69
+ }
70
+ }
71
+
72
+ public pause(): void {
73
+ const { playerOptions } = this
74
+ if (playerOptions) {
75
+ playerOptions.paused = true
76
+ this.decoder.pause()
77
+ }
78
+ }
79
+
80
+ public stop(): void {
81
+ const { playerOptions } = this
82
+ if (playerOptions) {
83
+ playerOptions.nowIndex = 0
84
+ this.currentTime = 0
85
+ this.pause()
86
+ }
87
+ }
88
+ }
package/src/Video.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { IVideo, IVideoInputData, IVideoData, IVideoDecoder, ILeafPaint } from '@leafer-ui/interface'
2
+ import { Image, registerUI, dataProcessor, useModule } from '@leafer-ui/draw'
3
+
4
+ import { VideoData } from './data/VideoData'
5
+ import { PaintVideo } from './PaintVideo'
6
+
7
+
8
+ @useModule(PaintVideo)
9
+ @registerUI()
10
+ export class Video<TInputData = IVideoInputData> extends Image<TInputData> implements IVideo {
11
+
12
+ public get __tag() { return 'Video' }
13
+
14
+ @dataProcessor(VideoData)
15
+ declare public __: IVideoData
16
+
17
+ public autoplay?: boolean
18
+
19
+ public muted?: boolean
20
+
21
+ public loop?: boolean
22
+ }
23
+
24
+ export interface Video {
25
+
26
+ readonly playerOptions: ILeafPaint
27
+ readonly decoder: IVideoDecoder
28
+
29
+ duration: number // 总时长
30
+ currentTime: number // 当前播放进度 (秒)
31
+ volume: number // 音量 0 ~1
32
+
33
+ readonly paused: boolean
34
+ readonly ended: boolean // 是否播放结束
35
+
36
+ togglePlay(): void
37
+ play(): void
38
+ pause(): void
39
+ stop(): void
40
+ }
@@ -0,0 +1,118 @@
1
+ import { MathHelper } from '@leafer-ui/draw'
2
+ import { ICanvasContext2D, IVideoDecoder, IVideoFileType, ILeaf, IVideoPlayOptions } from '@leafer-ui/interface'
3
+
4
+
5
+ interface IVideoDecoderMap {
6
+ [name: string]: { Decoder: any, force?: boolean }
7
+ }
8
+
9
+ export class VideoDecoder implements IVideoDecoder {
10
+
11
+ public width: number // 宽度
12
+ public height: number // 高度
13
+
14
+ public duration: number
15
+
16
+ public get currentTime(): number { // 获取当前播放进度 (秒)
17
+ const video = this.decoder
18
+ return video.currentTime
19
+ }
20
+
21
+ public set currentTime(time: number) { // 设置播放进度
22
+ const video = this.decoder
23
+ const targetTime = MathHelper.within(time, 0, this.duration)
24
+ video.currentTime = targetTime
25
+ }
26
+
27
+ public get volume(): number { // 获取当前音量 (0 ~ 1)
28
+ const video = this.decoder
29
+ return video.volume
30
+ }
31
+
32
+ public set volume(volume: number) { // 设置音量 (0 ~ 1)
33
+ const video = this.decoder
34
+ const targetVolume = MathHelper.within(volume, 0, 1)
35
+ video.volume = targetVolume
36
+ if (targetVolume > 0 && video.muted) video.muted = false // 如果音量大于 0,确保解除静音状态
37
+ }
38
+
39
+ public get paused(): boolean {
40
+ const video = this.decoder
41
+ return video.paused
42
+ }
43
+
44
+ public get ended(): boolean {
45
+ const video = this.decoder
46
+ return video.ended
47
+ }
48
+
49
+ public loop: number // 循环次数,为0表示无限循环
50
+
51
+ public decoder: HTMLVideoElement // 解码器
52
+
53
+ // 缓冲画布
54
+ public bufferCanvas?: any
55
+ public bufferContext?: ICanvasContext2D
56
+
57
+ static decoderMap: IVideoDecoderMap = {}
58
+
59
+ static register(type: IVideoFileType, Decoder: any, force?: boolean): void {
60
+ VideoDecoder.decoderMap[type] = { Decoder, force }
61
+ }
62
+
63
+ public load(): Promise<void> {
64
+ return undefined
65
+ }
66
+
67
+ public play(): void {
68
+ const video = this.decoder
69
+ video.play()
70
+ }
71
+
72
+ public pause(): void {
73
+ const video = this.decoder
74
+ video.pause()
75
+ }
76
+
77
+
78
+ public render(canvas: ICanvasContext2D, x: number, y: number, width: number, height: number, leaf: ILeaf, playOptions: IVideoPlayOptions, _imageScaleX: number, _imageScaleY: number): void {
79
+ let { paused } = playOptions
80
+
81
+ const video = this.decoder
82
+ if (video.ended) {
83
+ video.pause()
84
+ playOptions.paused = true
85
+ playOptions.loop = (playOptions.loop as number || 0) + 1
86
+ }
87
+
88
+ if (paused === false && video.paused) {
89
+ this.play()
90
+ } else if (paused === true && !video.paused) {
91
+ this.pause()
92
+ } else {
93
+ playOptions.paused = video.paused
94
+ }
95
+
96
+ // 仅在视频处于播放状态,且有实际数据更新时进行绘制
97
+ canvas.drawImage(video, x, y, width, height)
98
+
99
+ if (!video.paused) leaf.forceRender() // 防止停掉
100
+ }
101
+
102
+
103
+ public destroyDecoder(): void {
104
+ const video = this.decoder
105
+ if (video) {
106
+
107
+ video.pause()
108
+ video.src = ''
109
+ video.load()
110
+ this.decoder = null
111
+ }
112
+ }
113
+
114
+ public close(): void {
115
+ this.destroyDecoder()
116
+ }
117
+
118
+ }
@@ -0,0 +1,9 @@
1
+ import { IMultimediaType } from '@leafer-ui/interface'
2
+ import { ImageData } from '@leafer-ui/draw'
3
+
4
+
5
+ export class VideoData extends ImageData {
6
+
7
+ public get __urlType(): IMultimediaType { return 'video' }
8
+
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { LeaferVideo, Platform, Creator, PaintImage, Resource, Plugin } from '@leafer-ui/draw'
2
+ import { ILeaferVideo, ILeaferVideoConfig, IImageCrossOrigin, ILeaf, ILeafPaint, IVideoFileType } from '@leafer-ui/interface'
3
+
4
+ import { VideoDecoder } from './VideoDecoder'
5
+
6
+ export { Video } from './Video'
7
+ export { VideoData } from './data/VideoData'
8
+ export { VideoDecoder } from './VideoDecoder'
9
+ export { PaintVideo } from './PaintVideo'
10
+
11
+
12
+ Plugin.add('video')
13
+
14
+
15
+ PaintImage.video = function (paint: ILeafPaint): void {
16
+ paint.film = true
17
+ paint.nowIndex = 0
18
+ }
19
+
20
+ Creator.video = function (options?: ILeaferVideoConfig): ILeaferVideo {
21
+ return new LeaferVideo(options)
22
+ }
23
+
24
+ Resource.loadVideo = function (key: string, format?: IVideoFileType): Promise<ILeaferVideo> {
25
+ return new Promise((resolve, reject) => {
26
+ const config: ILeaferVideoConfig = { url: key }
27
+ if (format) config.format = format
28
+ const video = Creator.video(config)
29
+ Resource.set(key, video)
30
+ video.load(() => resolve(video), (e: any) => reject(e))
31
+ })
32
+ }
33
+
34
+ LeaferVideo.prototype.render = function (canvas: any, x: number, y: number, width: number, height: number, leaf: ILeaf, paint: any, imageScaleX: number, imageScaleY: number): void {
35
+ this.view.render(canvas, x, y, width, height, leaf, paint, imageScaleX, imageScaleY)
36
+ }
37
+
38
+
39
+ Platform.origin.loadVideo = async function (url: string, _crossOrigin?: IImageCrossOrigin, _leaferVideo?: ILeaferVideo): Promise<any> {
40
+ //const buffer = await Platform.origin.loadContent(url, 'arrayBuffer')
41
+
42
+ // let ext = getFileExt(url) // 获取文件后缀名
43
+
44
+ // if (!ext) {
45
+ // if (leaferVideo) ext = leaferVideo.config.format
46
+ // if (!ext) ext = 'gif'
47
+ // }
48
+
49
+ // const mimeType = FileHelper.mimeType(ext)
50
+ const video = createVideoElement(url)
51
+
52
+ const decoder = new VideoDecoder()
53
+
54
+ const { width, height, duration } = await waitVideoMetadata(video)
55
+ decoder.decoder = video
56
+ decoder.width = width
57
+ decoder.height = height
58
+ decoder.duration = duration
59
+
60
+ await waitVideoFrameReady(video)
61
+
62
+ return decoder
63
+ }
64
+
65
+ /**
66
+ * 创建并配置一个隐藏的 HTMLVideoElement
67
+ */
68
+ function createVideoElement(url: string, muted = false, loop = false): HTMLVideoElement {
69
+ const video = document.createElement('video')
70
+ video.src = url
71
+ video.muted = muted
72
+ video.loop = loop
73
+
74
+ // 必须设置 playsInline 保证在移动端浏览器内联播放,而不是弹窗全屏
75
+ video.playsInline = true
76
+ //video.webkitPlaysInline = true
77
+
78
+ // 允许跨域(如果视频托管在 CDN,Canvas 绘制需要此属性避免 Canvas 污染/报错)
79
+ video.crossOrigin = 'anonymous'
80
+
81
+ return video
82
+ }
83
+
84
+ /**
85
+ * 等待视频元数据加载完毕,返回视频的尺寸与时长信息
86
+ */
87
+ function waitVideoMetadata(video: HTMLVideoElement): Promise<{ width: number; height: number; duration: number }> {
88
+ return new Promise((resolve) => {
89
+ if (video.readyState >= 1) { // HAVE_METADATA 及以上
90
+ resolve({
91
+ width: video.videoWidth,
92
+ height: video.videoHeight,
93
+ duration: video.duration
94
+ })
95
+ } else {
96
+ video.addEventListener('loadedmetadata', () => {
97
+ resolve({
98
+ width: video.videoWidth,
99
+ height: video.videoHeight,
100
+ duration: video.duration
101
+ })
102
+ }, { once: true })
103
+ }
104
+ })
105
+ }
106
+
107
+ /**
108
+ * 等待视频首帧可绘制到 Canvas
109
+ */
110
+ export function waitVideoFrameReady(video: HTMLVideoElement): Promise<void> {
111
+ return new Promise((resolve) => {
112
+ if ('requestVideoFrameCallback' in video) {
113
+ video.requestVideoFrameCallback(() => resolve())
114
+ return
115
+ }
116
+
117
+ if ((video as any).readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
118
+ resolve()
119
+ return
120
+ }
121
+
122
+ (video as any).addEventListener('loadeddata', () => resolve(), {
123
+ once: true
124
+ })
125
+ })
126
+ }
@@ -0,0 +1,12 @@
1
+ import { IBox, IBoxData, IBoxInputData } from '@leafer-ui/interface' // IVideo, IVideoData, IVideoInputData,
2
+
3
+ // VideoBox
4
+ export interface IVideoBox extends IVideoAttrData, IBox {
5
+ __: IVideoBoxData
6
+ }
7
+ export interface IVideoBoxData extends IVideoAttrData, IBoxData { }
8
+ export interface IVideoBoxInputData extends IVideoAttrData, IBoxInputData { }
9
+
10
+ interface IVideoAttrData {
11
+ url?: string
12
+ }
@@ -0,0 +1,82 @@
1
+ import { IVideoInputData, IVideo, IVideoData, ILeafPaint, IVideoDecoder, IMultimediaType, ICanvasContext2D, IVideoFileType, ILeaf, IVideoPlayOptions, IUI } from '@leafer-ui/interface';
2
+ import { Image, ImageData } from '@leafer-ui/draw';
3
+
4
+ declare class Video<TInputData = IVideoInputData> extends Image<TInputData> implements IVideo {
5
+ get __tag(): string;
6
+ __: IVideoData;
7
+ autoplay?: boolean;
8
+ muted?: boolean;
9
+ loop?: boolean;
10
+ }
11
+ interface Video {
12
+ readonly playerOptions: ILeafPaint;
13
+ readonly decoder: IVideoDecoder;
14
+ duration: number;
15
+ currentTime: number;
16
+ volume: number;
17
+ readonly paused: boolean;
18
+ readonly ended: boolean;
19
+ togglePlay(): void;
20
+ play(): void;
21
+ pause(): void;
22
+ stop(): void;
23
+ }
24
+
25
+ declare class VideoData extends ImageData {
26
+ get __urlType(): IMultimediaType;
27
+ }
28
+
29
+ interface IVideoDecoderMap {
30
+ [name: string]: {
31
+ Decoder: any;
32
+ force?: boolean;
33
+ };
34
+ }
35
+ declare class VideoDecoder implements IVideoDecoder {
36
+ width: number;
37
+ height: number;
38
+ duration: number;
39
+ get currentTime(): number;
40
+ set currentTime(time: number);
41
+ get volume(): number;
42
+ set volume(volume: number);
43
+ get paused(): boolean;
44
+ get ended(): boolean;
45
+ loop: number;
46
+ decoder: HTMLVideoElement;
47
+ bufferCanvas?: any;
48
+ bufferContext?: ICanvasContext2D;
49
+ static decoderMap: IVideoDecoderMap;
50
+ static register(type: IVideoFileType, Decoder: any, force?: boolean): void;
51
+ load(): Promise<void>;
52
+ play(): void;
53
+ pause(): void;
54
+ render(canvas: ICanvasContext2D, x: number, y: number, width: number, height: number, leaf: ILeaf, playOptions: IVideoPlayOptions, _imageScaleX: number, _imageScaleY: number): void;
55
+ destroyDecoder(): void;
56
+ close(): void;
57
+ }
58
+
59
+ declare class PaintVideo {
60
+ element: IUI;
61
+ get playerOptions(): ILeafPaint;
62
+ get decoder(): IVideoDecoder;
63
+ get duration(): number;
64
+ get currentTime(): number;
65
+ set currentTime(time: number);
66
+ get volume(): number;
67
+ set volume(volume: number);
68
+ get paused(): boolean;
69
+ get ended(): boolean;
70
+ constructor(element: IUI);
71
+ togglePlay(): void;
72
+ play(): void;
73
+ pause(): void;
74
+ stop(): void;
75
+ }
76
+
77
+ /**
78
+ * 等待视频首帧可绘制到 Canvas
79
+ */
80
+ declare function waitVideoFrameReady(video: HTMLVideoElement): Promise<void>;
81
+
82
+ export { PaintVideo, Video, VideoData, VideoDecoder, waitVideoFrameReady };