WebController - Enter Frame

The enter frame section contains mostly methods that are triggered each frame or in a similar way.

onNewFrame

onNewFrame(frameSource)

The onNewFrame method performs various tasks that need constant updating. We call it every frame for the Genvid client we created.

  1. First we update the overlays to adapt to the composition of the video stream through updateOverlays(compositionData). This allows for croping of overlay in picture in picture mode.
        updateOverlays(compositionData) {
            let multipleSourceVideoComposition = false;
            let hideOverlay = false;
            // Do we have multiple sources?
            if (compositionData && compositionData.length > 1) {
                // Assuming the second element to be the foreground frame.
                if (compositionData[1].type == "PipVideoLayout") {
                    // Prevent the 3d overlay to overlay the secondary screen
                    this.pipFrameDiv.style.display = "block";
                    hideOverlay = true;
                    multipleSourceVideoComposition = true;
                    // set up the main canvas to pip affine transform matrix
                    const pipMat = genvidMath.mat3FromArray(compositionData[1].affineMatrix);
                    this.updateDomRect(this.pipFrameDiv, this.videoOverlay, pipMat);
                    this.updateDomClipping(this.canvas3d, pipMat);
                } else if (compositionData[1].type == "ChromaKeyVideoLayout") {
                    multipleSourceVideoComposition = true;
                }
            } 
            if (!hideOverlay) {
                this.pipFrameDiv.style.display = "none";
                if (this.canvas3d) this.canvas3d.style.removeProperty("clip-path");
            }
            return multipleSourceVideoComposition;
        }
    
  2. We get the annotation data, verify that it’s valid, and display a console log afterwards.

  3. We verify that the video is ready to perform operations.

  4. If the video is ready, we get the game data and verify that this data is valid before doing any operation with it.

  5. Once we are certain about the data validity, we get the object list from the data (cubes).

  6. We update the text field for the time left until the next camera and scene.

  7. We verify if we set up the player table. If that isn’t the case, we call initPlayerTable.

  8. We use the objects array to create a loop that creates the circles made in WebGL.

  9. With the same loop, we update the popularity value and the position value for each object.

  10. After the loop, we draw the WebGL circles.

            if (this.videoReady) {
                if (gameDataFrame && gameDataFrame.user) {
                    
                    const gameData = gameDataFrame.user;
                    this.lastGameData = gameData;
                    const cubes = gameData.cubes;
    
                    // Lets update the time of camera and scene. Only for the Unity sample.
                    if (gameData.camera && gameData.scene) {
                        this.timeCamSceneDiv.textContent = ` Time until next camera: ${gameData.camera.toFixed(2)} seconds
                    Time until next scene: ${gameData.scene.toFixed(2)} seconds`;
                    }
    
    
                    // Performs the webgl update process -- Update 3d
                    if (cubes) {
                        // Sets up the player table once the game data is ready
                        if (this.cubePanelDiv.length === 0) {
                            this.initPlayerTable(cubes);
                        }
    
                        const vertices = [];
                        const projViewMatrix = this.convertMatrix(gameData.MatProjView);
                        for (const cube of cubes) {
                            const m = cube.mat;
                            const p = genvidMath.vec3(m[12], m[13], m[14]);
                            const r = this.circleRadius;
                            let c = genvidMath.vec4(1, 0, 0, 1);
                            if (cube.color) {
                                c = genvidMath.vec4(cube.color[0], cube.color[1], cube.color[2], cube.color[3]);
                            }
                            if (this.isSelected(cube.name)) {
                                c = genvidMath.muls4D(c, 2.0); // brighten selected color intensity
                            } else {
                                c = genvidMath.muls4D(c, 0.8); // dim unselected color intensity
                            }
                            this.makeCircleZ(vertices, p.x, p.y, p.z, r, c);
    
                            // Makes the name tag of the cube follow the cube
                            const tag = this.findOrCreateTagDiv(cube);
                            const pos2d = genvidMath.projectPosition(projViewMatrix, p);
                            this.centerAt(tag, pos2d, genvidMath.vec2(0, -75));
    
                            // Modifies the popularity with the latest value
                            if (this.latestPopularity) {
                                const pCube = this.latestPopularity.find((c) => {
                                    return c.name === cube.name;
                                });
                                if (pCube) {
                                    cube.popularity = pCube.popularity;
                                }
                            }
    
                            let cubePopSpan = document.querySelector(`#${cube.name} .cheer_value`);
                            cubePopSpan.textContent = this.popularityToText(cube.popularity);
    
                            let cubePosXSpan = document.querySelector(`#${cube.name} .position_x`);
                            cubePosXSpan.textContent = cube.mat[12].toFixed(2);
    
                            let cubePosYSpan = document.querySelector(`#${cube.name} .position_y`);
                            cubePosYSpan.textContent = cube.mat[13].toFixed(2);
    
                            let cubePosZSpan = document.querySelector(`#${cube.name} .position_z`);
                            cubePosZSpan.textContent = cube.mat[14].toFixed(2);
                        }
    
                        let numQuads = vertices.length / (4 * 9);
    
                        this.gfxCmdCubes.vtx = this.genvidWebGL.createBuffer(new Float32Array(vertices));
                        this.gfxCmdCubes.idx = this.genvidWebGL.createIndexBufferForQuads(numQuads);
                    }
    
                    this.gfxProgDataViewproj = gameData.MatProjView;
    
                    this.gfxDraw3D();
                }
    
  11. Afterwards, we update the Genvid overlay values and the prompt-overlay visibility (used to display user changes to the sound).

                // Updates the Genvid information overlay
                let width = 18; // Width of the content of every line (without label).
                this.timeLocalDiv.textContent = `Local: ${this.preN(this.msToDuration(Date.now()), width, " ")}`;
    
                this.timeVideoDiv.textContent = `Est. Video: ${this.preN(this.msToDuration(Math.round(this.genvidClient.videoTimeMS)), width, " ")}`;
    
                this.timeComposeLastDiv.textContent = `Stream received: ${this.preN(this.msToDuration(Math.round(this.genvidClient.lastComposeTimeMS)), width, " ")}`;
    
                this.timeStreamDiv.textContent = `Stream played: ${this.preN(this.msToDuration(Math.round(this.genvidClient.streamTimeMS)), width, " ")}`;
    
                this.latencyDiv.textContent = `Latency: ${this.preN(this.genvidClient.streamLatencyMS.toFixed(0), width - 3, " ")} ms`;
    
                this.delayOffsetDiv.textContent = `DelayOffset: ${this.preN(this.genvidClient.delayOffset.toFixed(0), width - 3, " ")} ms`;
    
                // Updates the visibility on the overlay when using key press
                if (this.volumeDisplay.style.visibility === "visible" && this.volumeInfoDisplayCount < this.volumeInfoDisplayUntil) {
                    this.volumeInfoDisplayCount++;
                } else if (this.volumeDisplay.style.visibility === "visible" && this.volumeInfoDisplayCount >= this.volumeInfoDisplayUntil) {
                    this.volumeDisplay.style.visibility = "hidden";
                }
            }
    

initPlayerTable

initPlayerTable(cubeData)

The initPlayerTable creates a table under the stream window for each object.

  1. We start with a loop that repeats for each object’s information, based on received game data.

  2. We clone a node that we later append to an HTML tag.

  3. We then add the event listeners for the table, cheer, and reset click functionality.

  4. Finally, we perform a loop for each color available and add a click-event listener.

        // Method used to display the appropriate number of players with their proper buttons
        initPlayerTable(cubeData) {
            let cubePanel = document.getElementById("cube_panel_prototype");
    
            for (let cube of cubeData) {
                let cubePanelClone = cubePanel.cloneNode(true);
                cubePanelClone.id = cube.name;
                cubePanelClone.getElementsByClassName("cube_name")[0].innerText = cube.name;
    
                let cheerButton = cubePanelClone.querySelector(".cheer");
                cheerButton.addEventListener("click", () => this.onCheer(cube.name), false);
    
                let cubeDiv = cubePanelClone.querySelector(".cube");
                cubeDiv.addEventListener("click", () => this.onSelect(cube.name), false);
                this.cubePanelDiv.push(cubeDiv); // will stop triggering initPlayerTable from onNewFrame() indefinitely
    
                let resetButton = cubePanelClone.querySelector(".reset");
                resetButton.addEventListener("click", () => this.onReset(cube.name), false);
    
                for (let colorSelect of this.tableColor) {
                    let colorButton = cubePanelClone.querySelector("." + colorSelect[0]);
                    colorButton.addEventListener("click", () => this.onColorChange(cube.name, colorSelect[1]), false);
                }
                document.querySelector(".gameControlsDiv").append(cubePanelClone);
            }
            cubePanel.remove();
        }
    

onStreamsReceived

onStreamsReceived(dataStreams)

The onStreamsReceived method uses loops to get the game data. If the format received in the first loop is JSON, we convert the data into JavaScript. If the format is UTF8 and the ID is “GameCopyright”, we display it with a console.log. In the second loop we convert the JSON data from the annotations into JavaScript. We call onStreamsReceived when the website receives the stream.

    // Upon receving the stream, gets the data
    onStreamsReceived(dataStreams) {
        for (let stream of dataStreams.streams) {
            // Using switch...case because different operations can be made depending on the stream ID.
            switch (this.streamIdToFormat[stream.id]) {
                case "JSON": {
                    // Parse the JSON from each elements
                    for (let frame of stream.frames) {
                        frame.user = JSON.parse(frame.data);
                    }
                    break;
                }
                case "UTF8": {
                    // Code handling UTF8 data format.
                    for (let frame of stream.frames) {
                        if (stream.id == "GameCopyright") genvid.log(frame.data);
                    }
                    break;
                }
                default:
                    break;
            }
        }
        for (let annotation of dataStreams.annotations) {
            // Using switch...case because different operations can be made depending on the stream ID.
            switch (this.annotationIdToFormat[annotation.id]) {
                case "JSON": {
                    // Parse the JSON from each elements
                    for (let frame of annotation.frames) {
                        frame.user = JSON.parse(frame.data);
                    }
                    break;
                }
                default:
                    break;
            }
        }
    }

onNotificationsReceived

onNotificationsReceived(message)

The onNotificationsReceived method checks if any notification’s ID is Popularity. When it is, we convert the JSON data into JavaScript. We call this method when the website receives a notification.

    // Upon receiving a notification, gets the notification content
    onNotificationsReceived(message) {
        for (let notification of message.notifications) {
            if (notification.id === "Popularity") {
                this.latestPopularity = JSON.parse(notification.data).cubes;
            }
        }
    }