Files
GDevelop/Extensions/Video/videoruntimeobject.ts
2025-07-15 13:38:23 +02:00

437 lines
12 KiB
TypeScript

namespace gdjs {
/** The initial properties for {@link gdjs.VideoRuntimeObject} */
export type VideoObjectDataType = {
/** The base parameters of the video */
content: {
/** The opacity of the video */
opacity: number;
/** Does the video loops itself? */
loop: boolean;
/** The volume of the video */
volume: number;
/** Name of the resource corresponding to the video */
videoResource: string;
};
};
export type VideoObjectData = ObjectData & VideoObjectDataType;
export type VideoNetworkSyncDataType = {
op: float;
// We don't sync volume, as it's probably a user setting?
pla: boolean;
loop: boolean;
ct: float;
ps: number;
};
export type VideoNetworkSyncData = ObjectNetworkSyncData &
VideoNetworkSyncDataType;
/**
* An object displaying a video on screen.
*
* For the same video resource, only one video is being created in memory (
* as a HTMLVideoElement). This means that two objects displaying the same
* video will have the same state for this video (paused/playing, current time,
* volume, etc...).
*/
export class VideoRuntimeObject
extends gdjs.RuntimeObject
implements gdjs.OpacityHandler
{
_opacity: float;
_loop: boolean;
_volume: float;
_videoResource: string;
// Use a boolean to track if the video was paused because we
// navigated to another scene, and so should resume if we're back.
_pausedAsScenePaused: boolean = false;
_renderer: gdjs.VideoRuntimeObjectRenderer;
_playbackSpeed: any;
/**
* @param instanceContainer The scene the object belongs to.
* @param videoObjectData The data defining the object
*/
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
videoObjectData: VideoObjectData
) {
super(instanceContainer, videoObjectData);
this._opacity = videoObjectData.content.opacity;
this._loop = videoObjectData.content.loop;
this._volume = videoObjectData.content.volume;
this._videoResource = videoObjectData.content.videoResource;
this._renderer = new gdjs.VideoRuntimeObjectRenderer(
this,
instanceContainer
);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
getRendererObject() {
return this._renderer.getRendererObject();
}
updateFromObjectData(
oldObjectData: VideoObjectData,
newObjectData: VideoObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (oldObjectData.content.loop !== newObjectData.content.loop) {
this.setLoop(newObjectData.content.loop);
}
if (oldObjectData.content.volume !== newObjectData.content.volume) {
this.setVolume(newObjectData.content.volume);
}
if (
oldObjectData.content.videoResource !==
newObjectData.content.videoResource
) {
return false;
}
return true;
}
getNetworkSyncData(): VideoNetworkSyncData {
return {
...super.getNetworkSyncData(),
op: this._opacity,
pla: this.isPlayed(),
loop: this.isLooped(),
ct: this.getCurrentTime(),
ps: this.getPlaybackSpeed(),
};
}
updateFromNetworkSyncData(syncData: VideoNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
if (this._opacity !== undefined && this._opacity && syncData.op) {
this.setOpacity(syncData.op);
}
if (syncData.pla !== undefined && this.isPlayed() !== syncData.pla) {
syncData.pla ? this.play() : this.pause();
}
if (syncData.loop !== undefined && this.isLooped() !== syncData.loop) {
this.setLoop(syncData.loop);
}
// We don't update the current time too regularly, only if it's off by a lot.
if (
syncData.ct !== undefined &&
Math.abs(this.getCurrentTime() - syncData.ct) > 3 // More than 3 seconds off
) {
this.setCurrentTime(syncData.ct);
}
if (
syncData.ps !== undefined &&
this.getPlaybackSpeed() !== syncData.ps
) {
this.setPlaybackSpeed(syncData.ps);
}
}
/**
* Initialize the extra parameters that could be set for an instance.
* @param initialInstanceData The initial instance data
*/
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
if (initialInstanceData.opacity !== undefined) {
this.setOpacity(initialInstanceData.opacity);
}
}
onDestroyed(): void {
super.onDestroyed();
this._renderer.onDestroy();
}
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
this._renderer.ensureUpToDate();
}
/**
* Set object position on X axis.
* @param x The new position X of the object.
*/
setX(x: float): void {
super.setX(x);
this._renderer.updatePosition();
}
/**
* Set object position on Y axis.
* @param y The new position Y of the object.
*/
setY(y: float): void {
super.setY(y);
this._renderer.updatePosition();
}
/**
* Set the angle of the object.
* @param angle The new angle of the object.
*/
setAngle(angle: float): void {
super.setAngle(angle);
this._renderer.updateAngle();
}
/**
* Set object opacity.
* @param opacity The new opacity of the object (0-255).
*/
setOpacity(opacity: float): void {
this._opacity = opacity;
this._renderer.updateOpacity();
}
/**
* Get object opacity.
* @returns The current opacity
*/
getOpacity(): number {
return this._opacity;
}
/**
* Set the width of the video.
* @param width The new width in pixels.
*/
setWidth(width: float): void {
if (this._renderer.getWidth() === width) return;
this._renderer.setWidth(width);
this.invalidateHitboxes();
}
/**
* Set the height of the video.
* @param height The new height in pixels.
*/
setHeight(height: float): void {
if (this._renderer.getHeight() === height) return;
this._renderer.setHeight(height);
this.invalidateHitboxes();
}
/**
* Get the width of the video object.
* @returns The current width of the object
*/
getWidth(): float {
return this._renderer.getWidth();
}
/**
* Get the height of the video object.
* @returns The current height of the object
*/
getHeight(): float {
return this._renderer.getHeight();
}
/**
* Play the video.
*/
play(): void {
this._renderer.play();
}
/**
* Pause the video.
*/
pause(): void {
this._renderer.pause();
}
/**
* Set the state looped of the video.
* @param enable true to loop the video
*/
setLoop(enable: boolean): void {
this._renderer.setLoop(enable);
}
/**
* Set the state muted of the video.
* @param enable The new state.
*/
mute(enable: boolean) {
this._renderer.setMute(enable);
}
/**
* Return the state muted of video object.
* @returns Is the video muted?
*/
isMuted(): boolean {
return this._renderer.isMuted();
}
/**
* Set the volume of the video object.
* @param volume The new volume, between 0 and 100.
*/
setVolume(volume: number): void {
this._volume =
gdjs.evtTools.common.clamp(
gdjs.evtTools.common.normalize(volume, 0, 100),
0,
1
) * 100;
this._renderer.updateVolume();
}
/**
* Get the volume of the video object.
* @returns The current video's volume, between 0 and 100.
*/
getVolume(): number {
return (
gdjs.evtTools.common.normalize(this._renderer.getVolume(), 0, 1) * 100
);
}
/**
* Check if the video is being played.
* @returns Is the video being played?
*/
isPlayed(): boolean {
return this._renderer.isPlayed();
}
/**
* Check if the video is paused.
* @returns Is the video being paused?
*/
isPaused(): boolean {
return !this._renderer.isPlayed();
}
/**
* Check if the video is looping.
* @returns Is the video looping?
*/
isLooped(): boolean {
return this._renderer.isLooped();
}
/**
* Return the total time of the video.
* @returns The duration of the video
*/
getDuration(): number {
return this._renderer.getDuration();
}
/**
* Check if the video has ended.
* @returns Has the video ended?
*/
isEnded(): boolean {
return this._renderer.isEnded();
}
/**
* Set the new time of the video object.
* @param time The new time.
*/
setCurrentTime(time: float): void {
this._renderer.setCurrentTime(time);
}
/**
* Get the current time of the video object.
* @returns The current time of the video
*/
getCurrentTime(): float {
return this._renderer.getCurrentTime();
}
/**
* Set the new playback speed of the video object.
* @param playbackSpeed The new playback speed.
*/
setPlaybackSpeed(playbackSpeed: number): void {
this._playbackSpeed = gdjs.evtTools.common.clamp(playbackSpeed, 0.5, 2);
this._renderer.setPlaybackSpeed(this._playbackSpeed);
}
/**
* Get the playback speed of the video object.
* @returns The current playback speed of the video.
*/
getPlaybackSpeed(): number {
return this._renderer.getPlaybackSpeed();
}
}
gdjs.registerObject('Video::VideoObject', gdjs.VideoRuntimeObject);
/**
* When a scene is unloaded, pause any video being run.
* TODO: Investigate how to dispose the video source?
*/
gdjs.registerRuntimeSceneUnloadedCallback(function (runtimeScene) {
// Manually find all the gdjs.VideoRuntimeObject living on the scene,
// and pause them.
const instances = runtimeScene.getAdhocListOfAllInstances();
for (let i = 0; i < instances.length; ++i) {
const obj = instances[i];
if (obj instanceof gdjs.VideoRuntimeObject) {
if (obj.isPlayed()) {
obj.pause();
}
}
}
});
/**
* When a scene is paused, pause any video being run.
*/
gdjs.registerRuntimeScenePausedCallback(function (runtimeScene) {
// Manually find all the gdjs.VideoRuntimeObject living on the scene,
// and pause them.
const instances = runtimeScene.getAdhocListOfAllInstances();
for (let i = 0; i < instances.length; ++i) {
const obj = instances[i];
if (obj instanceof gdjs.VideoRuntimeObject) {
if (obj.isPlayed()) {
obj.pause();
obj._pausedAsScenePaused = true;
}
}
}
});
// Flag it to be started again when scene is resumed.
/**
* When a scene is resumed, resume any video previously paused.
*/
gdjs.registerRuntimeSceneResumedCallback(function (runtimeScene) {
// Manually find all the gdjs.VideoRuntimeObject living on the scene,
// and play them if they have been previously paused.
const instances = runtimeScene.getAdhocListOfAllInstances();
for (let i = 0; i < instances.length; ++i) {
const obj = instances[i];
if (obj instanceof gdjs.VideoRuntimeObject) {
if (obj._pausedAsScenePaused) {
obj.play();
}
}
}
});
}