From 20dffae199941095e31eb59ccc7779171263d42f Mon Sep 17 00:00:00 2001 From: santipuppoQualabs Date: Mon, 9 Dec 2024 20:25:16 -0300 Subject: [PATCH] Feat #10: Added web component code replicating UI work so far in plain JS --- web_component_demo/public/index.html | 6 +- web_component_demo/public/src/video-moq.ts | 510 +++++++++++++++++---- 2 files changed, 423 insertions(+), 93 deletions(-) diff --git a/web_component_demo/public/index.html b/web_component_demo/public/index.html index d525b74..7f1dc61 100644 --- a/web_component_demo/public/index.html +++ b/web_component_demo/public/index.html @@ -8,9 +8,13 @@ +

Web Component Example

diff --git a/web_component_demo/public/src/video-moq.ts b/web_component_demo/public/src/video-moq.ts index 917e1cc..6ffc377 100644 --- a/web_component_demo/public/src/video-moq.ts +++ b/web_component_demo/public/src/video-moq.ts @@ -1,9 +1,27 @@ +import { Player } from "@kixelated/moq/playback"; + class VideoMoq extends HTMLElement { private shadow: ShadowRoot; - private audioElement: HTMLAudioElement; + + // Event Handlers + private playPauseEventHandler: (event: Event) => void; + private onMouseEnterHandler: (event: Event) => void; + private onMouseLeaveHandler: (event: Event) => void; + private toggleMuteEventHandler: (event: Event) => void; + private toggleShowTrackEventHandler: (event: Event) => void; + + // HTML Elements + private canvas: HTMLCanvasElement; private playButton: HTMLButtonElement; - private pauseButton: HTMLButtonElement; - private progressBar: HTMLInputElement; + private controls: HTMLElement; + private volumeButton: HTMLButtonElement; + private trackButton: HTMLButtonElement; + private trackList: HTMLUListElement; + + // State + private player: Player | null = null; + public isMuted: boolean = true; // TODO: have the player state maintain all values + public selectedTrack: string = "0"; constructor() { super(); @@ -11,124 +29,432 @@ class VideoMoq extends HTMLElement { // Attach Shadow DOM this.shadow = this.attachShadow({ mode: "open" }); this.shadow.innerHTML = ` - -
- - - -
- `; + if (!src) { + this.error("No 'src' attribute provided for "); + } + if (src === null || namespace === null || fingerprint === null) return; - this.audioElement = document.createElement("audio"); - this.playButton = this.shadow.querySelector("#play")!; - this.pauseButton = this.shadow.querySelector("#pause")!; - this.progressBar = this.shadow.querySelector("#progress")!; + // TODO: make tracknum a parameter somehow + const trackNum = 0; + Player.create({ url: src, fingerprint, canvas: this.canvas, namespace }, trackNum) + .then((player) => this.setPlayer(player)) + .catch(this.error); + + this.canvas.addEventListener("click", this.playPauseEventHandler); + + const controls = this.getAttribute("controls"); + if (controls !== null) { + this.playButton.addEventListener("click", this.playPauseEventHandler); - // Bind event listeners - this.playButton.addEventListener("click", this.playAudio.bind(this)); - this.pauseButton.addEventListener("click", this.pauseAudio.bind(this)); - this.progressBar.addEventListener("input", this.seekAudio.bind(this)); + this.volumeButton.addEventListener("click", this.toggleMuteEventHandler); + + this.canvas.addEventListener("mouseenter", this.onMouseEnterHandler); + this.canvas.addEventListener("mouseleave", this.onMouseLeaveHandler); + this.controls.addEventListener("mouseenter", this.onMouseEnterHandler); + this.controls.addEventListener("mouseleave", this.onMouseLeaveHandler); + + this.trackButton.addEventListener("click", this.toggleShowTrackEventHandler); + } + } + + /** + * Called when the element is removed from the DOM + * */ + disconnectedCallback() { + this.canvas.removeEventListener("click", this.playPauseEventHandler); + this.playButton.removeEventListener("click", this.playPauseEventHandler); + + this.volumeButton.removeEventListener("click", this.toggleMuteEventHandler); + + this.canvas.removeEventListener("mouseenter", this.onMouseEnterHandler); + this.canvas.removeEventListener("mouseleave", this.onMouseLeaveHandler); + this.controls.removeEventListener("mouseenter", this.onMouseEnterHandler); + this.controls.removeEventListener("mouseleave", this.onMouseLeaveHandler); + + this.trackButton.removeEventListener("click", this.toggleShowTrackEventHandler); + + this.player?.close(); } - // connectedCallback: Called when the element is first added to the DOM - // disconnectedCallback: Called when the element is removed from the DOM // attributeChangedCallbackCalled when one of the element's watched attributes change. For an attribute to be watched, you must add it to the component class's static observedAttributes property. + // TODO: Move attribute processing to a function and add this. - // src - // width - // height - // controls - // autoplay - // muted - // poster - connectedCallback() { - const src = this.getAttribute("src"); - if (src) { - this.audioElement.src = src; - this.audioElement.addEventListener("timeupdate", this.updateProgress.bind(this)); - this.audioElement.addEventListener("ended", this.onAudioEnded.bind(this)); + /** + * Sets the player attribute and configures info related to a successful connection + * */ + private setPlayer(player: Player) { + this.player = player; + + if (!this.player.isPaused()) { + this.playButton.innerHTML = PAUSE_SVG; + this.playButton.ariaLabel = "Pause"; + + // TODO: This is a hacky !isMuted() + if (this.player.getAudioTracks().length > 0) { + this.volumeButton.ariaLabel = "Mute"; + this.volumeButton.innerText = "🔊"; + this.isMuted = true; + } else { + this.isMuted = false; + } + } + + const options = this.player.getVideoTracks(); + this.trackList.innerHTML = options + .map((option) => { + return ``; + }) + .join(""); + this.trackList.querySelectorAll("li").forEach((element) => { + element.addEventListener("click", () => this.switchTrack(element.dataset.name || null)); + element.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + this.switchTrack(element.dataset.name || null); + } + }); + }); + } + + private toggleShowControls(show: boolean) { + if (show) { + this.controls.classList.add("opacity-100"); + this.controls.classList.remove("opacity-0"); } else { - console.error("No 'src' attribute provided for "); + this.controls.classList.add("opacity-0"); + this.controls.classList.remove("opacity-100"); } } - // Play the audio - private playAudio() { - this.audioElement.play(); + // Play / Pause + private togglePlayPause() { this.playButton.disabled = true; - this.pauseButton.disabled = false; + this.player + ?.play() + .then(() => { + if (this.player?.isPaused()) { + this.playButton.innerHTML = PLAY_SVG; + this.playButton.ariaLabel = "Play"; + } else { + this.playButton.innerHTML = PAUSE_SVG; + this.playButton.ariaLabel = "Pause"; + } + }) + .finally(() => (this.playButton.disabled = false)); + } + + private toggleMute() { + this.volumeButton.disabled = true; + this.player + ?.mute(this.isMuted) + .then(() => { + // This is unintuitive but you should read it as if it wasMuted + if (this.isMuted) { + this.volumeButton.ariaLabel = "Mute"; + this.volumeButton.innerText = "🔇"; + this.isMuted = false; + } else { + this.volumeButton.ariaLabel = "Mute"; + this.volumeButton.innerText = "🔊"; + this.isMuted = true; + } + }) + .finally(() => { + this.volumeButton.disabled = false; + }); + } + + private toggleShowTracks() { + this.trackList.classList.toggle("opacity-0"); + } + + private switchTrack(name: string | null) { + if (name === null) { + this.error("Could not recognize selected track name"); + return; + } + + this.selectedTrack = name; + this.player?.switchTrack(name); } - // Pause the audio - private pauseAudio() { - this.audioElement.pause(); - this.playButton.disabled = false; - this.pauseButton.disabled = true; + // TODO: (?) Handle Stream ended event. May not be necessary, it came in the example. + // private onStreamEnded() { + // this.playButton.disabled = false; + // } + + /* Right now we are just printing errors, but we could + display them on the canvas if we want */ + private error(msg: any) { + console.error(msg); } +} - // Update progress bar - private updateProgress() { - const progress = (this.audioElement.currentTime / this.audioElement.duration) * 100; - this.progressBar.value = progress.toString(); +// There may be some repeated stuff here. I just copied from +// each element in dev tools and cleaned up some classes. +// Feel free to modify. +/** + * This stylesheet is self contained within the shadow root + * If we attach the element as open in the constructor, it should inherit + * the document's style. + */ +const STYLE = ``; +const PLAY_SVG = ` + + `; +const PAUSE_SVG = ` + + `; // Register the custom element customElements.define("player-component", VideoMoq); export default VideoMoq;