@zcomponent/three-video-player 1.0.0-beta.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/README.md +148 -0
- package/index.d.ts +1 -0
- package/index.js +4 -0
- package/lib/components/basevideo.d.ts +152 -0
- package/lib/components/basevideo.js +230 -0
- package/lib/components/videoplayer.d.ts +33 -0
- package/lib/components/videoplayer.js +94 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +2 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @zcomponent/three-video-player
|
|
2
|
+
|
|
3
|
+
This package adds support for video playback in 3D environments to the Mattercraft 3D content creation platform for the web. It allows you to easily integrate and control video content within your 3D scenes, supporting various video formats and transparency options.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Supports both HLS (.m3u8) and MP4 video formats
|
|
8
|
+
- Offers various video transparency types, including chroma key and side-by-side transparency
|
|
9
|
+
- Provides control over video playback properties like volume, looping, and muting
|
|
10
|
+
- Seamlessly integrates with the Mattercraft 3D environment
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
To use the VideoPlayer component in your Mattercraft project:
|
|
15
|
+
|
|
16
|
+
1. Right-click on a Group node in your scene's Hierarchy.
|
|
17
|
+
2. Navigate to the "Media" submenu.
|
|
18
|
+
3. Select "VideoPlayer" to add it to your scene.
|
|
19
|
+
|
|
20
|
+
## Video Formats
|
|
21
|
+
|
|
22
|
+
The VideoPlayer supports two primary video formats:
|
|
23
|
+
|
|
24
|
+
- HLS (.m3u8): For adaptive streaming
|
|
25
|
+
- MP4: For standard video playback
|
|
26
|
+
|
|
27
|
+
## Transparency Options
|
|
28
|
+
|
|
29
|
+
The VideoPlayer offers three transparency types:
|
|
30
|
+
|
|
31
|
+
1. None: Standard video playback without transparency
|
|
32
|
+
2. Chroma Key: Removes a specific color from the video, typically used for green screen effects
|
|
33
|
+
3. Side-by-Side: Uses a side-by-side video format where one side contains the alpha channel
|
|
34
|
+
|
|
35
|
+
## Key Properties
|
|
36
|
+
|
|
37
|
+
- `source`: The URL or file path of the video (supports .m3u8 and .mp4 files)
|
|
38
|
+
- `transparent`: The type of transparency to apply (none, chromaKey, or sideBySide)
|
|
39
|
+
- `autoplay`: Whether the video should start playing automatically
|
|
40
|
+
- `volume`: The playback volume (0 to 1)
|
|
41
|
+
- `muted`: Whether the video is muted
|
|
42
|
+
- `loop`: Whether the video should loop when it reaches the end
|
|
43
|
+
|
|
44
|
+
## Chroma Key Properties
|
|
45
|
+
|
|
46
|
+
When using chroma key transparency:
|
|
47
|
+
|
|
48
|
+
- `similarity`: How closely a color must match the key color to be made transparent
|
|
49
|
+
- `smoothness`: The smoothness of the chroma key edges
|
|
50
|
+
- `spill`: Controls color spill reduction
|
|
51
|
+
- `keyColor`: The color to be made transparent (default is green: [0, 1, 0])
|
|
52
|
+
|
|
53
|
+
## Side-by-Side Properties
|
|
54
|
+
|
|
55
|
+
When using side-by-side transparency:
|
|
56
|
+
|
|
57
|
+
- `direction`: The direction of the side-by-side split (LeftRight or TopBottom)
|
|
58
|
+
- `alphaFirst`: Whether the alpha channel is on the left/top (true) or right/bottom (false)
|
|
59
|
+
|
|
60
|
+
## Playback Control
|
|
61
|
+
|
|
62
|
+
The VideoPlayer provides several methods for controlling playback:
|
|
63
|
+
|
|
64
|
+
- `play()`: Starts or resumes playback
|
|
65
|
+
- `pause()`: Pauses playback
|
|
66
|
+
- `stop()`: Stops playback and resets to the beginning
|
|
67
|
+
- `seek(time)`: Jumps to a specific time in the video (in milliseconds)
|
|
68
|
+
|
|
69
|
+
## Events
|
|
70
|
+
|
|
71
|
+
The VideoPlayer emits several events that you can listen to:
|
|
72
|
+
|
|
73
|
+
- `onEnded`: Fired when the video playback ends
|
|
74
|
+
- `onPause`: Fired when the video is paused
|
|
75
|
+
- `onPlay`: Fired when video playback starts
|
|
76
|
+
- `onPlaying`: Fired when the video starts playing after being paused or stopped for buffering
|
|
77
|
+
- `onWaiting`: Fired when the video stops because it needs to buffer the next frame
|
|
78
|
+
- `onError`: Fired when an error occurs during video loading or playback
|
|
79
|
+
|
|
80
|
+
## Example Usage
|
|
81
|
+
|
|
82
|
+
Here's a simple example of how to use the VideoPlayer in a custom behavior:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Behavior, ContextManager } from '@zcomponent/core';
|
|
86
|
+
import { VideoPlayer } from '@zcomponent/three-video-player';
|
|
87
|
+
/**
|
|
88
|
+
* @zbehavior
|
|
89
|
+
*/
|
|
90
|
+
export class MyVideoBehavior extends Behavior<VideoPlayer> {
|
|
91
|
+
constructor(contextManager: ContextManager, instance: VideoPlayer) {
|
|
92
|
+
super(contextManager, instance);
|
|
93
|
+
|
|
94
|
+
// Play the video when it's loaded
|
|
95
|
+
this.instance.load().then(() => {
|
|
96
|
+
this.instance.play();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Listen for the video to end
|
|
100
|
+
this.register(this.instance.onEnded, () => {
|
|
101
|
+
console.log('Video playback ended');
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Here's an example of dynamically creating a VideoPlayer component in a custom Three.js component:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { Component, ContextManager, registerLoadable } from '@zcomponent/core';
|
|
111
|
+
import { Group } from '@zcomponent/three/lib/components/Group';
|
|
112
|
+
import { VideoPlayer, VideoTransparencyType } from '@zcomponent/three-video-player';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @zcomponent
|
|
116
|
+
*/
|
|
117
|
+
export class CustomVideoComponent extends Group {
|
|
118
|
+
private videoPlayer: VideoPlayer;
|
|
119
|
+
|
|
120
|
+
constructor(contextManager: ContextManager, props: {}) {
|
|
121
|
+
super(contextManager, {});
|
|
122
|
+
|
|
123
|
+
// Create a new VideoPlayer instance
|
|
124
|
+
const source = new URL('./path/to/your/video.mp4', import.meta.url).href;
|
|
125
|
+
|
|
126
|
+
this.videoPlayer = new VideoPlayer(contextManager, {
|
|
127
|
+
source,
|
|
128
|
+
transparent: VideoTransparencyType.none,
|
|
129
|
+
autoplay: false
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Add the VideoPlayer's element to our Group
|
|
133
|
+
this.appendChild.add(this.videoPlayer);
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Example method to start playback
|
|
138
|
+
playVideo() {
|
|
139
|
+
this.videoPlayer.play();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Don't forget to clean up when the component is disposed
|
|
143
|
+
dispose() {
|
|
144
|
+
this.videoPlayer.dispose();
|
|
145
|
+
return super.dispose();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib';
|
package/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { ContextManager, Event, Observable, PlayOptions, Stream } from '@zcomponent/core';
|
|
3
|
+
import { Object3D } from '@zcomponent/three/lib/components/Object3D';
|
|
4
|
+
import { MeshChromaKeyMaterial } from '@zcomponent/three/lib/internal/MeshChromaKeyMaterial';
|
|
5
|
+
import { MeshSideBySideAlphaMaterial, SideBySideDirection } from '@zcomponent/three/lib/internal/MeshSideBySideAlphaMaterial';
|
|
6
|
+
export declare enum VideoTransparencyType {
|
|
7
|
+
none = "none",
|
|
8
|
+
chromaKey = "chromaKey",
|
|
9
|
+
sideBySide = "sideBySide"
|
|
10
|
+
}
|
|
11
|
+
export interface BaseVideoConstructorProps {
|
|
12
|
+
geometry?: THREE.BufferGeometry;
|
|
13
|
+
/**
|
|
14
|
+
* Support videos with transparency
|
|
15
|
+
*
|
|
16
|
+
* @zprop
|
|
17
|
+
* @zdefault none
|
|
18
|
+
*/
|
|
19
|
+
transparent: VideoTransparencyType;
|
|
20
|
+
/** @zprop
|
|
21
|
+
* @zdefault false
|
|
22
|
+
*/
|
|
23
|
+
autoplay: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* @zprop
|
|
26
|
+
* @zdefault default
|
|
27
|
+
*/
|
|
28
|
+
audioLayer?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Abstract base class for video components in a 3D environment.
|
|
32
|
+
* Supports various video transparency types, including chroma key and side-by-side transparency.
|
|
33
|
+
*
|
|
34
|
+
* Root element: [THREE.Group](https://threejs.org/docs/index.html#api/en/objects/Group)
|
|
35
|
+
* @zicon video_library
|
|
36
|
+
* @ztag three/Object3D/BaseVideo
|
|
37
|
+
* @zparents three/Object3D/Group/**
|
|
38
|
+
* @zgroup Media
|
|
39
|
+
* @zstream
|
|
40
|
+
*/
|
|
41
|
+
export declare abstract class BaseVideo extends Object3D implements Stream {
|
|
42
|
+
protected constructorProps: BaseVideoConstructorProps;
|
|
43
|
+
onEnded: Event<[BaseVideo]>;
|
|
44
|
+
onPause: Event<[BaseVideo]>;
|
|
45
|
+
onPlay: Event<[BaseVideo]>;
|
|
46
|
+
onPlaying: Event<[BaseVideo]>;
|
|
47
|
+
onWaiting: Event<[BaseVideo]>;
|
|
48
|
+
onError: Event<[BaseVideo]>;
|
|
49
|
+
element: THREE.Group;
|
|
50
|
+
geometry: THREE.BufferGeometry;
|
|
51
|
+
material: THREE.MeshBasicMaterial | MeshChromaKeyMaterial | MeshSideBySideAlphaMaterial;
|
|
52
|
+
mesh: THREE.Mesh;
|
|
53
|
+
texture: THREE.VideoTexture;
|
|
54
|
+
transparent: VideoTransparencyType;
|
|
55
|
+
videoElement: HTMLVideoElement;
|
|
56
|
+
protected _gain: GainNode;
|
|
57
|
+
protected _hadUserEvent: boolean;
|
|
58
|
+
constructor(ctx: ContextManager, constructorProps: BaseVideoConstructorProps);
|
|
59
|
+
private _updateMute;
|
|
60
|
+
/**
|
|
61
|
+
* The volume of the video, from 0 to 1.
|
|
62
|
+
* @zprop
|
|
63
|
+
* @ztype proportion
|
|
64
|
+
* @zdefault 1
|
|
65
|
+
*/
|
|
66
|
+
volume: Observable<number, never>;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the video is muted.
|
|
69
|
+
* @zprop
|
|
70
|
+
* @zdefault false
|
|
71
|
+
*/
|
|
72
|
+
muted: Observable<boolean, never>;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the video should loop when it reaches the end.
|
|
75
|
+
* @zprop
|
|
76
|
+
* @zdefault false
|
|
77
|
+
*/
|
|
78
|
+
loop: Observable<boolean, never>;
|
|
79
|
+
/**
|
|
80
|
+
* Plays the video.
|
|
81
|
+
* @zprop
|
|
82
|
+
* @zui
|
|
83
|
+
*/
|
|
84
|
+
play(opts?: PlayOptions): void;
|
|
85
|
+
/**
|
|
86
|
+
* Pauses the video.
|
|
87
|
+
* @zprop
|
|
88
|
+
* @zui
|
|
89
|
+
*/
|
|
90
|
+
pause(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Stops the video and resets the playback position to the beginning.
|
|
93
|
+
* @zprop
|
|
94
|
+
* @zui
|
|
95
|
+
*/
|
|
96
|
+
stop(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Seeks to a specified time in the video in milliseconds.
|
|
99
|
+
* @zprop
|
|
100
|
+
* @zui
|
|
101
|
+
*/
|
|
102
|
+
seek(/** @ztype time-milliseconds */ t: number): void;
|
|
103
|
+
length(): number | undefined;
|
|
104
|
+
isStalled(): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The chroma key similarity value, from 0 to 1.
|
|
107
|
+
* @zprop
|
|
108
|
+
* @zgroup Chroma Key Transparency
|
|
109
|
+
* @ztype proportion
|
|
110
|
+
* @zdefault 0.05
|
|
111
|
+
*/
|
|
112
|
+
similarity: Observable<number, never>;
|
|
113
|
+
/**
|
|
114
|
+
* The smoothness of the chroma key, from 0 to 1.
|
|
115
|
+
* @zprop
|
|
116
|
+
* @zgroup Chroma Key Transparency
|
|
117
|
+
* @ztype proportion
|
|
118
|
+
* @zdefault 0.08
|
|
119
|
+
*/
|
|
120
|
+
smoothness: Observable<number, never>;
|
|
121
|
+
/**
|
|
122
|
+
* The spill of the chroma key, from 0 to 1.
|
|
123
|
+
* @zprop
|
|
124
|
+
* @zgroup Chroma Key Transparency
|
|
125
|
+
* @ztype proportion
|
|
126
|
+
* @zdefault 0.2
|
|
127
|
+
*/
|
|
128
|
+
spill: Observable<number, never>;
|
|
129
|
+
/**
|
|
130
|
+
* The key color of the chroma key, as an RGB array.
|
|
131
|
+
* @zprop
|
|
132
|
+
* @zgroup Chroma Key Transparency
|
|
133
|
+
* @ztype color-norm-rgb
|
|
134
|
+
* @zdefault [0, 1, 0]
|
|
135
|
+
*/
|
|
136
|
+
keyColor: Observable<[number, number, number], never>;
|
|
137
|
+
/**
|
|
138
|
+
* The direction of the side-by-side transparency.
|
|
139
|
+
* @zprop
|
|
140
|
+
* @zgroup Side By Side Transparency
|
|
141
|
+
* @zdefault LeftRight
|
|
142
|
+
*/
|
|
143
|
+
direction: Observable<SideBySideDirection, never>;
|
|
144
|
+
/**
|
|
145
|
+
* Whether the alpha channel is first or second in the side-by-side transparency.
|
|
146
|
+
* @zprop
|
|
147
|
+
* @zgroup Side By Side Transparency
|
|
148
|
+
* @zdefault false
|
|
149
|
+
*/
|
|
150
|
+
alphaFirst: Observable<boolean, never>;
|
|
151
|
+
dispose(): never;
|
|
152
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { AudioContextContext, Event, isDesignTime, nextUserEvent, Observable, registerLoadable, started, useOnBeforeRender } from '@zcomponent/core';
|
|
3
|
+
import { Object3D } from '@zcomponent/three/lib/components/Object3D';
|
|
4
|
+
import { MeshChromaKeyMaterial } from '@zcomponent/three/lib/internal/MeshChromaKeyMaterial';
|
|
5
|
+
import { MeshSideBySideAlphaMaterial, SideBySideDirection } from '@zcomponent/three/lib/internal/MeshSideBySideAlphaMaterial';
|
|
6
|
+
import { EncodingType } from '@zcomponent/three/lib';
|
|
7
|
+
import { legacy_set_texture_encoding } from '@zcomponent/three/lib/utils';
|
|
8
|
+
export var VideoTransparencyType;
|
|
9
|
+
(function (VideoTransparencyType) {
|
|
10
|
+
VideoTransparencyType["none"] = "none";
|
|
11
|
+
VideoTransparencyType["chromaKey"] = "chromaKey";
|
|
12
|
+
VideoTransparencyType["sideBySide"] = "sideBySide";
|
|
13
|
+
})(VideoTransparencyType || (VideoTransparencyType = {}));
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base class for video components in a 3D environment.
|
|
16
|
+
* Supports various video transparency types, including chroma key and side-by-side transparency.
|
|
17
|
+
*
|
|
18
|
+
* Root element: [THREE.Group](https://threejs.org/docs/index.html#api/en/objects/Group)
|
|
19
|
+
* @zicon video_library
|
|
20
|
+
* @ztag three/Object3D/BaseVideo
|
|
21
|
+
* @zparents three/Object3D/Group/**
|
|
22
|
+
* @zgroup Media
|
|
23
|
+
* @zstream
|
|
24
|
+
*/
|
|
25
|
+
export class BaseVideo extends Object3D {
|
|
26
|
+
constructor(ctx, constructorProps) {
|
|
27
|
+
super(ctx, constructorProps, new THREE.Group());
|
|
28
|
+
this.constructorProps = constructorProps;
|
|
29
|
+
this.onEnded = new Event();
|
|
30
|
+
this.onPause = new Event();
|
|
31
|
+
this.onPlay = new Event();
|
|
32
|
+
this.onPlaying = new Event();
|
|
33
|
+
this.onWaiting = new Event();
|
|
34
|
+
this.onError = new Event();
|
|
35
|
+
this._hadUserEvent = false;
|
|
36
|
+
this._updateMute = () => {
|
|
37
|
+
if (!this.videoElement || !this._hadUserEvent)
|
|
38
|
+
return;
|
|
39
|
+
this.videoElement.muted = this.muted.value;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The volume of the video, from 0 to 1.
|
|
43
|
+
* @zprop
|
|
44
|
+
* @ztype proportion
|
|
45
|
+
* @zdefault 1
|
|
46
|
+
*/
|
|
47
|
+
this.volume = new Observable(1, v => (this._gain.gain.value = v));
|
|
48
|
+
/**
|
|
49
|
+
* Whether the video is muted.
|
|
50
|
+
* @zprop
|
|
51
|
+
* @zdefault false
|
|
52
|
+
*/
|
|
53
|
+
this.muted = new Observable(false);
|
|
54
|
+
/**
|
|
55
|
+
* Whether the video should loop when it reaches the end.
|
|
56
|
+
* @zprop
|
|
57
|
+
* @zdefault false
|
|
58
|
+
*/
|
|
59
|
+
this.loop = new Observable(false, v => {
|
|
60
|
+
this.videoElement.loop = v;
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* The chroma key similarity value, from 0 to 1.
|
|
64
|
+
* @zprop
|
|
65
|
+
* @zgroup Chroma Key Transparency
|
|
66
|
+
* @ztype proportion
|
|
67
|
+
* @zdefault 0.05
|
|
68
|
+
*/
|
|
69
|
+
this.similarity = new Observable(0.05);
|
|
70
|
+
/**
|
|
71
|
+
* The smoothness of the chroma key, from 0 to 1.
|
|
72
|
+
* @zprop
|
|
73
|
+
* @zgroup Chroma Key Transparency
|
|
74
|
+
* @ztype proportion
|
|
75
|
+
* @zdefault 0.08
|
|
76
|
+
*/
|
|
77
|
+
this.smoothness = new Observable(0.08);
|
|
78
|
+
/**
|
|
79
|
+
* The spill of the chroma key, from 0 to 1.
|
|
80
|
+
* @zprop
|
|
81
|
+
* @zgroup Chroma Key Transparency
|
|
82
|
+
* @ztype proportion
|
|
83
|
+
* @zdefault 0.2
|
|
84
|
+
*/
|
|
85
|
+
this.spill = new Observable(0.2);
|
|
86
|
+
/**
|
|
87
|
+
* The key color of the chroma key, as an RGB array.
|
|
88
|
+
* @zprop
|
|
89
|
+
* @zgroup Chroma Key Transparency
|
|
90
|
+
* @ztype color-norm-rgb
|
|
91
|
+
* @zdefault [0, 1, 0]
|
|
92
|
+
*/
|
|
93
|
+
this.keyColor = new Observable([0, 1, 0]);
|
|
94
|
+
/**
|
|
95
|
+
* The direction of the side-by-side transparency.
|
|
96
|
+
* @zprop
|
|
97
|
+
* @zgroup Side By Side Transparency
|
|
98
|
+
* @zdefault LeftRight
|
|
99
|
+
*/
|
|
100
|
+
this.direction = new Observable(SideBySideDirection.LeftRight);
|
|
101
|
+
/**
|
|
102
|
+
* Whether the alpha channel is first or second in the side-by-side transparency.
|
|
103
|
+
* @zprop
|
|
104
|
+
* @zgroup Side By Side Transparency
|
|
105
|
+
* @zdefault false
|
|
106
|
+
*/
|
|
107
|
+
this.alphaFirst = new Observable(false);
|
|
108
|
+
const videoElement = document.createElement('video');
|
|
109
|
+
videoElement.crossOrigin = 'anonymous';
|
|
110
|
+
videoElement.playsInline = true;
|
|
111
|
+
videoElement.muted = true;
|
|
112
|
+
videoElement.preload = 'auto';
|
|
113
|
+
videoElement.onended = () => this.onEnded.emit(this);
|
|
114
|
+
videoElement.onpause = () => this.onPause.emit(this);
|
|
115
|
+
videoElement.onplay = () => this.onPlay.emit(this);
|
|
116
|
+
videoElement.onplaying = () => this.onPlaying.emit(this);
|
|
117
|
+
videoElement.onwaiting = () => this.onWaiting.emit(this);
|
|
118
|
+
videoElement.onerror = () => this.onError.emit(this);
|
|
119
|
+
this.geometry = constructorProps.geometry ?? new THREE.PlaneGeometry();
|
|
120
|
+
registerLoadable(ctx, new Promise((resolve, reject) => {
|
|
121
|
+
videoElement.addEventListener('canplaythrough', () => {
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
videoElement.addEventListener('error', err => {
|
|
125
|
+
console.error(err);
|
|
126
|
+
reject();
|
|
127
|
+
});
|
|
128
|
+
}));
|
|
129
|
+
const audioContextContext = ctx.get(AudioContextContext);
|
|
130
|
+
this._gain = new GainNode(audioContextContext.audioContext);
|
|
131
|
+
const audioSource = audioContextContext.audioContext.createMediaElementSource(videoElement);
|
|
132
|
+
audioSource.connect(this._gain);
|
|
133
|
+
this._gain.connect(audioContextContext.getLayer(constructorProps.audioLayer));
|
|
134
|
+
this.transparent = constructorProps.transparent ?? VideoTransparencyType.none;
|
|
135
|
+
this.videoElement = videoElement;
|
|
136
|
+
this.texture = new THREE.VideoTexture(videoElement);
|
|
137
|
+
legacy_set_texture_encoding(this.texture, EncodingType.sRGBEncoding);
|
|
138
|
+
if (this.transparent === VideoTransparencyType.chromaKey) {
|
|
139
|
+
const mat = new MeshChromaKeyMaterial();
|
|
140
|
+
this.material = mat;
|
|
141
|
+
mat.map = this.texture;
|
|
142
|
+
this.register(this.similarity, v => (mat.similarity = v));
|
|
143
|
+
this.register(this.smoothness, v => (mat.smoothness = v));
|
|
144
|
+
this.register(this.spill, v => (mat.spill = v));
|
|
145
|
+
this.register(this.keyColor, v => (mat.keyColor = [...v]));
|
|
146
|
+
this.register(useOnBeforeRender(this.contextManager), () => mat.frame());
|
|
147
|
+
}
|
|
148
|
+
else if (this.transparent === VideoTransparencyType.sideBySide) {
|
|
149
|
+
const mat = new MeshSideBySideAlphaMaterial();
|
|
150
|
+
this.material = mat;
|
|
151
|
+
mat.map = this.texture;
|
|
152
|
+
this.register(this.alphaFirst, v => (mat.alphaFirst = v));
|
|
153
|
+
this.register(this.direction, v => (mat.direction = v));
|
|
154
|
+
this.register(useOnBeforeRender(this.contextManager), () => mat.frame());
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.material = new THREE.MeshBasicMaterial({ toneMapped: false, map: this.texture });
|
|
158
|
+
}
|
|
159
|
+
this.mesh = new THREE.Mesh(this.geometry, this.material);
|
|
160
|
+
this.element.add(this.mesh);
|
|
161
|
+
this.videoElement.addEventListener('loadedmetadata', () => {
|
|
162
|
+
let transparencyAspectFix = 1;
|
|
163
|
+
if (this.transparent === VideoTransparencyType.sideBySide) {
|
|
164
|
+
transparencyAspectFix = this.direction.value === SideBySideDirection.LeftRight ? 0.5 : 2;
|
|
165
|
+
}
|
|
166
|
+
this.mesh.scale.set((transparencyAspectFix * this.videoElement.videoWidth) / this.videoElement.videoHeight, 1, 1);
|
|
167
|
+
});
|
|
168
|
+
this.register(this.muted, this._updateMute);
|
|
169
|
+
nextUserEvent(ctx).then(() => {
|
|
170
|
+
this._hadUserEvent = true;
|
|
171
|
+
this._updateMute();
|
|
172
|
+
});
|
|
173
|
+
if (!isDesignTime(ctx) && constructorProps.autoplay)
|
|
174
|
+
started(ctx).then(() => this.play());
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Plays the video.
|
|
178
|
+
* @zprop
|
|
179
|
+
* @zui
|
|
180
|
+
*/
|
|
181
|
+
play(opts) {
|
|
182
|
+
this.videoElement.playbackRate = opts?.speed ?? 1;
|
|
183
|
+
this.videoElement.play();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Pauses the video.
|
|
187
|
+
* @zprop
|
|
188
|
+
* @zui
|
|
189
|
+
*/
|
|
190
|
+
pause() {
|
|
191
|
+
this.videoElement.pause();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Stops the video and resets the playback position to the beginning.
|
|
195
|
+
* @zprop
|
|
196
|
+
* @zui
|
|
197
|
+
*/
|
|
198
|
+
stop() {
|
|
199
|
+
this.videoElement.pause();
|
|
200
|
+
this.videoElement.currentTime = 0;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Seeks to a specified time in the video in milliseconds.
|
|
204
|
+
* @zprop
|
|
205
|
+
* @zui
|
|
206
|
+
*/
|
|
207
|
+
seek(/** @ztype time-milliseconds */ t) {
|
|
208
|
+
this.videoElement.currentTime = t / 1000;
|
|
209
|
+
}
|
|
210
|
+
length() {
|
|
211
|
+
return isNaN(this.videoElement.duration) ? undefined : this.videoElement.duration * 1000;
|
|
212
|
+
}
|
|
213
|
+
isStalled() {
|
|
214
|
+
return this.videoElement.readyState <= 2;
|
|
215
|
+
}
|
|
216
|
+
dispose() {
|
|
217
|
+
this.geometry.dispose();
|
|
218
|
+
this.material.map?.dispose();
|
|
219
|
+
this.material.dispose();
|
|
220
|
+
this.texture.dispose();
|
|
221
|
+
this.videoElement.pause();
|
|
222
|
+
this.videoElement.onended = null;
|
|
223
|
+
this.videoElement.onpause = null;
|
|
224
|
+
this.videoElement.onplay = null;
|
|
225
|
+
this.videoElement.onplaying = null;
|
|
226
|
+
this.videoElement.onwaiting = null;
|
|
227
|
+
this.videoElement.onerror = null;
|
|
228
|
+
return super.dispose();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ContextManager } from '@zcomponent/core';
|
|
2
|
+
import { BaseVideo, BaseVideoConstructorProps } from './basevideo';
|
|
3
|
+
export interface VideoConstructorProps extends BaseVideoConstructorProps {
|
|
4
|
+
/** @zprop
|
|
5
|
+
* @zvalues files *.m3u8, *.mp4
|
|
6
|
+
*/
|
|
7
|
+
source: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* Manages the playback and display of a video in a 3D environment.
|
|
12
|
+
* Supports various video transparency types, including chroma key and side-by-side transparency.
|
|
13
|
+
* Provides control over video playback properties like volume, looping, and muting.
|
|
14
|
+
*
|
|
15
|
+
* Supports both HLS and MP4 video formats.
|
|
16
|
+
*
|
|
17
|
+
*
|
|
18
|
+
* Root element: [THREE.Group](https://threejs.org/docs/index.html#api/en/objects/Group)
|
|
19
|
+
* @zcomponent
|
|
20
|
+
* @zicon video_library
|
|
21
|
+
* @ztag three/Object3D/Video
|
|
22
|
+
* @zparents three/Object3D/Group/**
|
|
23
|
+
* @zgroup Media
|
|
24
|
+
*/
|
|
25
|
+
export declare class VideoPlayer extends BaseVideo {
|
|
26
|
+
protected constructorProps: VideoConstructorProps;
|
|
27
|
+
private hls;
|
|
28
|
+
constructor(ctx: ContextManager, constructorProps: VideoConstructorProps);
|
|
29
|
+
load(): Promise<void>;
|
|
30
|
+
private _loadHlsVideo;
|
|
31
|
+
private _loadMp4Video;
|
|
32
|
+
dispose(): never;
|
|
33
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// TODO- import type and dynamic import later
|
|
2
|
+
import Hls from 'hls.js';
|
|
3
|
+
import { BaseVideo } from './basevideo';
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* Manages the playback and display of a video in a 3D environment.
|
|
7
|
+
* Supports various video transparency types, including chroma key and side-by-side transparency.
|
|
8
|
+
* Provides control over video playback properties like volume, looping, and muting.
|
|
9
|
+
*
|
|
10
|
+
* Supports both HLS and MP4 video formats.
|
|
11
|
+
*
|
|
12
|
+
*
|
|
13
|
+
* Root element: [THREE.Group](https://threejs.org/docs/index.html#api/en/objects/Group)
|
|
14
|
+
* @zcomponent
|
|
15
|
+
* @zicon video_library
|
|
16
|
+
* @ztag three/Object3D/Video
|
|
17
|
+
* @zparents three/Object3D/Group/**
|
|
18
|
+
* @zgroup Media
|
|
19
|
+
*/
|
|
20
|
+
export class VideoPlayer extends BaseVideo {
|
|
21
|
+
constructor(ctx, constructorProps) {
|
|
22
|
+
super(ctx, constructorProps);
|
|
23
|
+
this.constructorProps = constructorProps;
|
|
24
|
+
this.hls = null;
|
|
25
|
+
}
|
|
26
|
+
async load() {
|
|
27
|
+
const isHls = this.constructorProps.source.endsWith('.m3u8');
|
|
28
|
+
return isHls ? this._loadHlsVideo() : this._loadMp4Video();
|
|
29
|
+
}
|
|
30
|
+
async _loadHlsVideo() {
|
|
31
|
+
if (!Hls.isSupported()) {
|
|
32
|
+
// Consider adding a fallback to MP4
|
|
33
|
+
console.error('HLS is not supported in this browser.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.hls = new Hls();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
this.hls?.on(Hls.Events.MEDIA_ATTACHED, () => { });
|
|
39
|
+
this.hls?.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
this.hls?.on(Hls.Events.ERROR, (event, data) => {
|
|
43
|
+
if (data.fatal) {
|
|
44
|
+
switch (data.type) {
|
|
45
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
46
|
+
console.error('HLS: Fatal network error encountered, trying to recover');
|
|
47
|
+
this.hls?.startLoad();
|
|
48
|
+
break;
|
|
49
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
50
|
+
console.error('HLS: Fatal media error encountered, trying to recover');
|
|
51
|
+
this.hls?.recoverMediaError();
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.error('HLS: Fatal error, cannot recover');
|
|
55
|
+
this.hls?.destroy();
|
|
56
|
+
reject('HLS: Fatal error, cannot recover');
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.hls?.loadSource(this.constructorProps.source);
|
|
62
|
+
this.hls?.attachMedia(this.videoElement);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async _loadMp4Video() {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const onCanPlay = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
resolve();
|
|
70
|
+
};
|
|
71
|
+
const onError = e => {
|
|
72
|
+
console.error('MP4: Error loading video', e);
|
|
73
|
+
cleanup();
|
|
74
|
+
reject(new Error('MP4 video loading error'));
|
|
75
|
+
};
|
|
76
|
+
const cleanup = () => {
|
|
77
|
+
this.videoElement.removeEventListener('canplay', onCanPlay);
|
|
78
|
+
this.videoElement.removeEventListener('error', onError);
|
|
79
|
+
};
|
|
80
|
+
this.videoElement.addEventListener('canplay', onCanPlay);
|
|
81
|
+
this.videoElement.addEventListener('error', onError);
|
|
82
|
+
this.videoElement.src = this.constructorProps.source;
|
|
83
|
+
this.videoElement.load();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
dispose() {
|
|
87
|
+
if (this.hls) {
|
|
88
|
+
this.hls.destroy();
|
|
89
|
+
this.hls = null;
|
|
90
|
+
}
|
|
91
|
+
this.videoElement.src = '';
|
|
92
|
+
return super.dispose();
|
|
93
|
+
}
|
|
94
|
+
}
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zcomponent/three-video-player",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "HLS & MP4 Player for Mattercraft",
|
|
5
|
+
"author": "Zappar Limited",
|
|
6
|
+
"license": "Proprietary",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"zexports": [
|
|
10
|
+
"./lib/**/*"
|
|
11
|
+
],
|
|
12
|
+
"directories": {
|
|
13
|
+
"lib": "lib"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"lib/**/*",
|
|
19
|
+
"public/**/*"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rm -rf lib && tsc",
|
|
23
|
+
"watch": "tsc -w",
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"docs": "typedoc --out docs"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"hls.js": "^1.5.12"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@zcomponent/three": "1.26.1-beta",
|
|
32
|
+
"three": "^0.150.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@zcomponent/three": "1.x",
|
|
36
|
+
"@zcomponent/core": "1.x",
|
|
37
|
+
"three": "0.150.1 - 164.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [],
|
|
40
|
+
"release": {
|
|
41
|
+
"extends": "semantic-release-commit-filter",
|
|
42
|
+
"ci": true,
|
|
43
|
+
"branches": [
|
|
44
|
+
"main",
|
|
45
|
+
{
|
|
46
|
+
"name": "beta",
|
|
47
|
+
"channel": "beta",
|
|
48
|
+
"prerelease": "beta"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "alpha",
|
|
52
|
+
"channel": "alpha",
|
|
53
|
+
"prerelease": "alpha"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"plugins": [
|
|
57
|
+
"@semantic-release/commit-analyzer",
|
|
58
|
+
[
|
|
59
|
+
"@semantic-release/release-notes-generator",
|
|
60
|
+
{
|
|
61
|
+
"writerOpts": {
|
|
62
|
+
"commitUrlFormat": null,
|
|
63
|
+
"issueUrlFormat": null,
|
|
64
|
+
"commitPartial": "* {{header}}{{#if subject}} {{subject}}{{/if}}"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"@semantic-release/changelog",
|
|
69
|
+
"@semantic-release/npm",
|
|
70
|
+
"@semantic-release/gitlab",
|
|
71
|
+
"@semantic-release/git"
|
|
72
|
+
],
|
|
73
|
+
"tagFormat": "@zcomponent/three-video-player@${version}"
|
|
74
|
+
}
|
|
75
|
+
}
|