diff --git a/browser/src/slideshow/SlideRenderer.ts b/browser/src/slideshow/SlideRenderer.ts index 2c5a09f66..8f053f34a 100644 --- a/browser/src/slideshow/SlideRenderer.ts +++ b/browser/src/slideshow/SlideRenderer.ts @@ -10,11 +10,18 @@ declare var SlideShow: any; +class VideoRenderInfo { + public texture: WebGLTexture; + public videoElement: HTMLVideoElement; + public vao: WebGLVertexArrayObject; +} + class SlideRenderer { public _context: RenderContext = null; private _program: WebGLProgram = null; private _vao: WebGLVertexArrayObject = null; public _slideTexture: WebGLTexture; + private _videos: VideoRenderInfo[]; private _canvas: HTMLCanvasElement; public getVertexShader(): string { @@ -49,8 +56,10 @@ class SlideRenderer { const gl = this._context.gl; const positions = new Float32Array([ - -1.0, -1.0, 0, 0, 1, 1.0, -1.0, 0, 1, 1, -1.0, 1.0, 0, 0, 0, 1.0, 1.0, 0, - 1, 0, + -1.0, -1.0, 0.0, 0.0, 1.0, + 1.0, -1.0, 0.0, 1.0, 1.0, + -1.0, 1.0, 0.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 1.0, 0.0, ]); const buffer = gl.createBuffer(); @@ -92,11 +101,97 @@ class SlideRenderer { return this._context.loadTexture(image); } - public renderFrame(currentSlideTexture: WebGLTexture) { + private setupVideo(url: string) : HTMLVideoElement { + const video = document.createElement("video"); + + video.playsInline = true; + video.muted = true; + video.loop = true; + + video.addEventListener("playing", () => { + // todo + }, true); + + video.addEventListener("timeupdate", () => { + // todo + }, true); + + video.src = url; + video.play(); + return video; + } + + private initTexture() { + const gl = this._context.gl; + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + + return texture; + } + + private updateTexture(texture: WebGLTexture, video: HTMLVideoElement) { + const gl = this._context.gl; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); + } + + public renderSlide(currentSlideTexture: WebGLTexture, slideInfo: SlideInfo, docWidth: number, docHeight: number) { this._slideTexture = currentSlideTexture; + this._videos = []; + for (var videoInfo of slideInfo.videos) { + const video = new VideoRenderInfo; + video.videoElement = this.setupVideo(videoInfo.url); + video.texture = this.initTexture(); + video.vao = this.setupVideoPosition(videoInfo.x, videoInfo.y, videoInfo.width, videoInfo.height, docWidth, docHeight); + + this._videos.push(video); + } requestAnimationFrame(this.render.bind(this)); } + setupVideoPosition(x: number, y: number, width: number, height: number, docWidth: number, docHeight: number): WebGLVertexArrayObject { + const gl = this._context.gl; + + var xMin = (x / docWidth) * 2.0 - 1.0; + var xMax = ((x + width) / docWidth) * 2.0 - 1.0; + + var yMin = (y / docHeight) * 2.0 - 1.0; + var yMax = ((y + height) / docHeight) * 2.0 - 1.0; + + const positions = new Float32Array([ + xMin, -yMin, 0.0, 0.0, 1.0, + xMax, -yMin, 0.0, 1.0, 1.0, + xMin, -yMax, 0.0, 0.0, 0.0, + xMax, -yMax, 0.0, 1.0, 0.0, + ]); + + const buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); + + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + + const positionLocation = gl.getAttribLocation(this._program, 'a_position'); + + gl.enableVertexAttribArray(positionLocation); + gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 5 * 4, 0); + + const texCoordLocation = gl.getAttribLocation(this._program, 'a_texCoord'); + + gl.enableVertexAttribArray(texCoordLocation); + gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 5 * 4, 3 * 4); + + return vao; + } + private render() { const gl = this._context.gl; gl.viewport(0, 0, this._canvas.width, this._canvas.height); @@ -111,5 +206,14 @@ class SlideRenderer { gl.bindVertexArray(this._vao); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + for (var video of this._videos) { + gl.bindVertexArray(video.vao); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + this.updateTexture(video.texture, video.videoElement); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + } + + requestAnimationFrame(this.render.bind(this)); } } diff --git a/browser/src/slideshow/SlideShowPresenter.ts b/browser/src/slideshow/SlideShowPresenter.ts index e0ffaecb1..c8bbd088e 100644 --- a/browser/src/slideshow/SlideShowPresenter.ts +++ b/browser/src/slideshow/SlideShowPresenter.ts @@ -14,12 +14,22 @@ declare var SlideShow: any; +interface VideoInfo { + id: number; + url: string; + x: number; + y: number; + width: number; + height: number; +} + interface SlideInfo { hash: string; index: number; empty: boolean; masterPage: string; masterPageObjectsVisibility: boolean; + videos: Array; transitionDuration: number; nextSlideDuration: number; transitionDirection: boolean; @@ -165,7 +175,7 @@ class SlideShowPresenter { transitionParameters.next = nextTexture; transitionParameters.slideInfo = slideInfo; transitionParameters.callback = () => { - this._slideRenderer.renderFrame(nextTexture); + this._slideRenderer.renderSlide(nextTexture, slideInfo, this._presentationInfo.docWidth, this._presentationInfo.docHeight); }; SlideShow.PerformTransition(transitionParameters); @@ -182,7 +192,8 @@ class SlideShowPresenter { this._slideCompositor.fetchAndRun(this._currentSlide, () => { const slideImage = this._slideCompositor.getSlide(this._currentSlide); const currentTexture = this._slideRenderer.createTexture(slideImage); - this._slideRenderer.renderFrame(currentTexture); + const slideInfo = this.getSlideInfo(this._currentSlide); + this._slideRenderer.renderSlide(currentTexture, slideInfo, this._presentationInfo.docWidth, this._presentationInfo.docHeight); }); }