Skip to content

Commit 304e642

Browse files
authored
Merge pull request #29 from Q42/bugfix/embed-video-improvements-2
Bugfix/embed video improvements 2
2 parents 5dfc782 + 05b828b commit 304e642

File tree

2 files changed

+163
-108
lines changed

2 files changed

+163
-108
lines changed

src/svelte/virtual/Embed.svelte

Lines changed: 14 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import type { Models } from '../../types/models';
33
import type { HTMLMicrioElement } from '../../ts/element';
44
import { writable, type Unsubscriber } from 'svelte/store';
5-
import type { HlsPlayer } from '../../types/externals';
65
7-
import { onMount, getContext, tick } from 'svelte';
6+
import { onMount, getContext } from 'svelte';
87
import { MicrioImage } from '../../ts/image';
9-
import { loadScript, once, createGUID, hasNativeHLS, Browser } from '../../ts/utils';
8+
import { once, createGUID, Browser } from '../../ts/utils';
9+
import { GLEmbedVideo } from '../../ts/embedvideo';
1010
1111
import Media from '../components/Media.svelte';
1212
@@ -122,11 +122,11 @@
122122
if((embed.video?.pauseWhenSmallerThan || embed.video?.pauseWhenLargerThan) && width) {
123123
const wasPaused:boolean = paused;
124124
paused = shouldPause();
125-
if(_vid && printGL && (paused != wasPaused)) {
126-
if(paused) _vid.pause();
125+
if(glVideo?._vid && (paused != wasPaused)) {
126+
if(paused) glVideo._vid.pause();
127127
else {
128-
if(mainImage?.$settings?.embedRestartWhenShown) _vid.currentTime = 0;
129-
_vid.play();
128+
if(mainImage?.$settings?.embedRestartWhenShown) glVideo._vid.currentTime = 0;
129+
glVideo._vid.play();
130130
}
131131
}
132132
}
@@ -151,107 +151,12 @@
151151
// If embed has ID of marker, watch if marker is closed to do media pre-destroy
152152
const destroying = writable<boolean>(false);
153153
154-
// WebGL-embedded video
155-
let hlsPlayer: HlsPlayer|undefined = undefined;
156-
let usVid:Unsubscriber|undefined = undefined;
157-
let _vid:HTMLVideoElement|undefined = image?._video;
158-
let vidRepeatTo:any = undefined;
159-
160154
if(embed.video) {
161155
if(!embed.video.controls) embed.video.muted = true;
162156
}
163157
164-
const setWebGLVideoPlaying = (playing:boolean) : void => {
165-
if(!_vid) return;
166-
if(!isMounted) playing = false;
167-
_vid.dataset.playing = playing ? '1' : undefined;
168-
wasm.e._setImageVideoPlaying(image.ptr, playing);
169-
if(embed.hideWhenPaused) wasm.fadeImage(image.ptr, playing ? 1 : 0);
170-
if(playing) wasm.render();
171-
}
172-
173-
function loadWebGLVideo() : void {
174-
if(!embed.video) return;
175-
176-
// Cloudflare stream doesn't support alpha transparent videos yet,
177-
// so use the original src if transparency is set to true.
178-
const ism3u = !!embed.video.streamId && !embed.video.transparent;
179-
const src = ism3u ? `https://videodelivery.net/${embed.video.streamId}/manifest/video.m3u8` : embed.video.src;
180-
_vid = document.createElement('video');
181-
_vid.crossOrigin = 'true';
182-
_vid.playsInline = true;
183-
_vid.width = embed.width! * .5;
184-
_vid.height = embed.height! * .5;
185-
_vid.muted = embed.video.muted;
186-
if($current && embed.id) $current.setEmbedMediaElement(embed.id, _vid);
187-
188-
const loopAfter = embed.video.loopAfter;
189-
if(embed.video.loop && loopAfter) {
190-
_vid.onended = () => {
191-
setWebGLVideoPlaying(false);
192-
vidRepeatTo = <any>setTimeout(() => _vid?.play(), loopAfter * 1000) as number;
193-
}
194-
_vid.onplay = () => setWebGLVideoPlaying(true);
195-
}
196-
else _vid.loop = embed.video.loop;
197-
198-
// If no autoplay, has to be rendered in DOM for first frame visibility
199-
if(!autoplay && !ism3u) {
200-
_vid.setAttribute('style','opacity:0;position:absolute;top:0;left:0;transform-origin:left top;transform:scale(0.1);pointer-events:none;');
201-
document.body.appendChild(_vid);
202-
}
203-
204-
_vid.addEventListener('play', () => setWebGLVideoPlaying(!paused));
205-
_vid.addEventListener('pause', () => setWebGLVideoPlaying(false));
206-
207-
// Only on first frame drawn, print the video
208-
_vid.addEventListener('playing', () => image.video.set(_vid), {once:true});
209-
210-
// OF COURSE certain iOS versions (iPhone 13..) don't fire the canplay-event
211-
_vid.addEventListener(Browser.iOS ? 'loadedmetadata' : 'canplay', () => {
212-
if(!_vid || !isMounted) return;
213-
// It could already be paused by scale limiting
214-
if(autoplay && !paused) {
215-
_vid.play();
216-
moved();
217-
}
218-
else if(!embed.hideWhenPaused) { // Show first frame
219-
setWebGLVideoPlaying(true);
220-
tick().then(() => {
221-
setWebGLVideoPlaying(false);
222-
setTimeout(() => _vid?.remove(),50);
223-
})
224-
}
225-
}, {once: true});
226-
227-
if(!ism3u || hasNativeHLS(_vid)) _vid.src = src;
228-
else loadScript('https://i.micr.io/hls-1.5.17.min.js', undefined, 'Hls' in window ? {} : undefined).then(() => {
229-
/** @ts-ignore */
230-
hlsPlayer = new (window['Hls'] as HlsPlayer)();
231-
hlsPlayer.loadSource(src);
232-
if(_vid) hlsPlayer.attachMedia(_vid);
233-
});
234-
}
235-
236-
let inScreen:boolean = false;
237-
238-
function printWebGLVideo() : void {
239-
let to:any;
240-
let first:boolean = true;
241-
usVid = image.visible.subscribe(v => {
242-
clearTimeout(to);
243-
inScreen = v;
244-
if(v) to = setTimeout(() => {
245-
if(!isMounted) return;
246-
if(!_vid) loadWebGLVideo();
247-
else if(autoplay) _vid.play();
248-
}, first ? 0 : 100);
249-
else to = setTimeout(() => _vid?.pause(), 0);
250-
first = false;
251-
})
252-
}
253-
254158
const isRawVideo = printGL && !!embed.video;
159+
let glVideo:GLEmbedVideo|undefined = undefined;
255160
function printInsideGL() : void {
256161
const opacity = embed.hideWhenPaused ? 0.01 : embed.opacity || 1;
257162
if(image && image.ptr >= 0) {
@@ -278,7 +183,10 @@
278183
}, embed.area, { opacity, asImage: false });
279184
}
280185
281-
if(isRawVideo) once(image.visible, {targetValue: true}).then(() => printWebGLVideo());
186+
if(isRawVideo) once(image.visible, {targetValue: true}).then(() => {
187+
// This takes care of loading and playing
188+
glVideo = new GLEmbedVideo(wasm, image, embed, paused, moved)
189+
});
282190
283191
wasm.render();
284192
}
@@ -288,7 +196,7 @@
288196
289197
// For <Media /> video embeds, set the embed.video.element on availability
290198
$: {
291-
if(embed.video && embed.id && $current) $current.setEmbedMediaElement(embed.id, _mediaElement);
199+
if(embed.video && embed.id && $current) $current.setEmbedMediaElement(embed.id, _mediaElement??glVideo?._vid);
292200
}
293201
294202
// Cap the max <video> element width/height to original video resolution
@@ -306,13 +214,11 @@
306214
307215
return () => {
308216
isMounted = false;
217+
glVideo?.unmount();
309218
if(image) {
310-
clearTimeout(vidRepeatTo);
311-
if(usVid) usVid();
312219
wasm.fadeImage(image.ptr, 0);
313220
wasm.render();
314221
}
315-
if(_vid) _vid.pause();
316222
if(embed.video && embed.id && $current) $current.setEmbedMediaElement(embed.id);
317223
while(us.length) us.shift()?.();
318224
}

src/ts/embedvideo.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { Unsubscriber } from 'svelte/motion';
2+
import type { HlsPlayer } from '../types/externals';
3+
import type { Models } from '../types/models';
4+
import type { MicrioImage } from './image';
5+
import type { Wasm } from './wasm';
6+
7+
import { Browser, hasNativeHLS, loadScript } from './utils';
8+
import { tick } from 'svelte';
9+
10+
/** In-WebGL rendered embedded videos, used in <Embed /> */
11+
export class GLEmbedVideo {
12+
private ism3u:boolean = false;
13+
private hlsPlayer: HlsPlayer|undefined = undefined;
14+
private usVid:Unsubscriber|undefined = undefined;
15+
private vidRepeatTo:any = undefined;
16+
private placeTo:any = undefined;
17+
private autoplay:boolean = true;
18+
19+
_vid:HTMLVideoElement|undefined = undefined;
20+
21+
isMounted:boolean = true;
22+
23+
constructor(
24+
private wasm:Wasm,
25+
private image:MicrioImage,
26+
private embed:Models.ImageData.Embed,
27+
private paused:boolean,
28+
private moved:() => void
29+
) {
30+
this.ism3u = !!embed.video?.streamId && !embed.video?.transparent;
31+
this._vid = image._video;
32+
this.autoplay = embed.video?.autoplay ?? true;
33+
34+
let first:boolean = true;
35+
this.usVid = this.image.visible.subscribe(v => {
36+
clearTimeout(this.placeTo);
37+
if(v) this.placeTo = setTimeout(() => {
38+
if(!this.isMounted) return;
39+
if(!this._vid) this.load();
40+
else {
41+
this.hook();
42+
if(this.autoplay) this._vid.play();
43+
}
44+
}, first ? 0 : 100);
45+
else this.placeTo = setTimeout(() => this._vid?.pause(), 0);
46+
first = false;
47+
});
48+
}
49+
50+
unmount() : void {
51+
this.isMounted = false;
52+
clearTimeout(this.placeTo);
53+
clearTimeout(this.vidRepeatTo);
54+
this._vid?.pause();
55+
this.unhook();
56+
this.usVid?.();
57+
}
58+
59+
private setPlaying(playing:boolean) : void {
60+
if(!this._vid) return;
61+
this.paused = !playing;
62+
this._vid.dataset.playing = playing ? '1' : undefined;
63+
this.wasm.e._setImageVideoPlaying(this.image.ptr, playing);
64+
if(this.embed.hideWhenPaused) this.wasm.fadeImage(this.image.ptr, playing ? 1 : 0);
65+
if(playing) this.wasm.render();
66+
}
67+
68+
private load() : void {
69+
if(!this.embed.video || this._vid) return;
70+
71+
// Cloudflare stream doesn't support alpha transparent videos yet,
72+
// so use the original src if transparency is set to true.
73+
const src = this.ism3u ? `https://videodelivery.net/${this.embed.video.streamId}/manifest/video.m3u8` : this.embed.video.src;
74+
this._vid = document.createElement('video');
75+
this._vid.crossOrigin = 'true';
76+
this._vid.playsInline = true;
77+
this._vid.width = this.embed.width! * .5;
78+
this._vid.height = this.embed.height! * .5;
79+
this._vid.muted = this.embed.video.muted;
80+
81+
this.hook();
82+
83+
if(!this.ism3u || hasNativeHLS(this._vid)) this._vid.src = src;
84+
else loadScript('https://i.micr.io/hls-1.5.17.min.js', undefined, 'Hls' in window ? {} : undefined).then(() => {
85+
/** @ts-ignore */
86+
this.hlsPlayer = new (window['Hls'] as HlsPlayer)();
87+
this.hlsPlayer.loadSource(src);
88+
if(this._vid) this.hlsPlayer.attachMedia(this._vid);
89+
});
90+
}
91+
92+
private events = {
93+
play: () => this.setPlaying(true),
94+
pause: () => this.setPlaying(false),
95+
// Only on first frame drawn, print the video
96+
playing: () => {if(!this.image._video) this.image.video.set(this._vid) },
97+
// OF COURSE certain iOS versions (iPhone 13..) don't fire the canplay-event
98+
canplayEvt: Browser.iOS ? 'loadedmetadata' : 'canplay',
99+
canplay:() => {
100+
if(!this._vid || !this.isMounted) return;
101+
// It could already be paused by scale limiting
102+
if(this.autoplay && !this.paused) {
103+
this._vid.play();
104+
this.moved();
105+
}
106+
else if(!this.embed.hideWhenPaused) { // Show first frame
107+
this.setPlaying(true);
108+
tick().then(() => {
109+
this.setPlaying(false);
110+
setTimeout(() => this._vid?.remove(),50);
111+
})
112+
}
113+
}
114+
}
115+
116+
private hook() {
117+
if(!this.embed.video || !this._vid) return;
118+
const loopAfter = this.embed.video.loopAfter;
119+
if(this.embed.video.loop && loopAfter) {
120+
this._vid.onended = () => {
121+
this.setPlaying(false);
122+
this.vidRepeatTo = <any>setTimeout(() => this._vid?.play(), loopAfter * 1000) as number;
123+
}
124+
this._vid.onplay = () => this.setPlaying(true);
125+
}
126+
else this._vid.loop = this.embed.video.loop;
127+
128+
// If no autoplay, has to be rendered in DOM for first frame visibility
129+
if(!this._vid.parentNode && !this.autoplay && !this.ism3u) {
130+
this._vid.setAttribute('style','opacity:0;position:absolute;top:0;left:0;transform-origin:left top;transform:scale(0.1);pointer-events:none;');
131+
document.body.appendChild(this._vid);
132+
}
133+
134+
this._vid.addEventListener('play', this.events.play);
135+
this._vid.addEventListener('pause', this.events.pause);
136+
this._vid.addEventListener('playing', this.events.playing, {once:true});
137+
this._vid.addEventListener(this.events.canplayEvt, this.events.canplay, {once: true});
138+
139+
}
140+
141+
private unhook() : void {
142+
if(!this._vid) return;
143+
this._vid.removeEventListener('play', this.events.play);
144+
this._vid.removeEventListener('pause', this.events.pause);
145+
this._vid.removeEventListener('playing', this.events.playing);
146+
this._vid.removeEventListener(this.events.canplayEvt, this.events.canplay);
147+
}
148+
149+
}

0 commit comments

Comments
 (0)