Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions js/hang-ui/src/Components/watch/QualitySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { For, type JSX, useContext } from "solid-js";
import { WatchUIContext } from "./WatchUIContextProvider";

export default function QualitySelector() {
const context = useContext(WatchUIContext);

const handleQualityChange: JSX.EventHandler<HTMLSelectElement, Event> = (event) => {
const selectedValue = event.currentTarget.value || undefined;
context?.setActiveRendition(selectedValue);
};

return (
<div class="qualitySelectorContainer">
<label for="quality-select" class="qualityLabel">
Quality:{" "}
</label>
<select
id="quality-select"
onChange={handleQualityChange}
class="qualitySelect"
value={context?.activeRendition() ?? ""}
>
<option value="">Auto</option>
<For each={context?.availableRenditions() ?? []}>
{(rendition) => (
<option value={rendition.name}>
{rendition.name}
{rendition.width && rendition.height ? ` (${rendition.width}x${rendition.height})` : ""}
</option>
)}
</For>
</select>
</div>
);
}
2 changes: 2 additions & 0 deletions js/hang-ui/src/Components/watch/WatchControls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FullscreenButton from "./FullscreenButton";
import LatencySlider from "./LatencySlider";
import PlayPauseButton from "./PlayPauseButton";
import QualitySelector from "./QualitySelector";
import VolumeSlider from "./VolumeSlider";
import WatchStatusIndicator from "./WatchStatusIndicator";

Expand All @@ -15,6 +16,7 @@ export default function WatchControls() {
</div>
<div class="latencyControlsRow">
<LatencySlider />
<QualitySelector />
</div>
</div>
);
Expand Down
45 changes: 45 additions & 0 deletions js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Time } from "@moq/hang";
import type * as Catalog from "@moq/hang/catalog";
import type HangWatch from "@moq/hang/watch/element";
import type { JSX } from "solid-js";
import { createContext, createEffect, createSignal } from "solid-js";
Expand All @@ -10,6 +11,12 @@ type WatchUIContextProviderProps = {

type WatchStatus = "no-url" | "disconnected" | "connecting" | "offline" | "loading" | "live" | "connected";

export type Rendition = {
name: string;
width?: number;
height?: number;
};

type WatchUIContextValues = {
hangWatch: () => HangWatch | undefined;
watchStatus: () => WatchStatus;
Expand All @@ -22,6 +29,9 @@ type WatchUIContextValues = {
buffering: () => boolean;
latency: () => number;
setLatencyValue: (value: number) => void;
availableRenditions: () => Rendition[];
activeRendition: () => string | undefined;
setActiveRendition: (name: string | undefined) => void;
};

export const WatchUIContext = createContext<WatchUIContextValues>();
Expand All @@ -33,6 +43,8 @@ export default function WatchUIContextProvider(props: WatchUIContextProviderProp
const [currentVolume, setCurrentVolume] = createSignal<number>(0);
const [buffering, setBuffering] = createSignal<boolean>(false);
const [latency, setLatency] = createSignal<number>(0);
const [availableRenditions, setAvailableRenditions] = createSignal<Rendition[]>([]);
const [activeRendition, setActiveRendition] = createSignal<string | undefined>(undefined);

const togglePlayback = () => {
const hangWatchEl = props.hangWatch();
Expand Down Expand Up @@ -66,6 +78,17 @@ export default function WatchUIContextProvider(props: WatchUIContextProviderProp
}
};

const setActiveRenditionValue = (name: string | undefined) => {
const hangWatchEl = props.hangWatch();

if (hangWatchEl) {
hangWatchEl.video.source.target.update((prev) => ({
...prev,
rendition: name,
}));
}
};
Comment on lines +81 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function repeats a pattern of getting hangWatch and checking if it exists. This pattern is also used in togglePlayback, setVolume, toggleMuted, and setLatencyValue. To reduce code duplication and improve maintainability, consider extracting this logic into a helper function.

For example, you could create a withHangWatch helper:

const withHangWatch = (fn: (hangWatch: HangWatch) => void) => {
    const hangWatchEl = props.hangWatch();
    if (hangWatchEl) {
        fn(hangWatchEl);
    }
};

Then, this function and others could be simplified:

const setActiveRenditionValue = (name: string | undefined) => {
    withHangWatch((hangWatch) => hangWatch.video.source.setActiveRendition(name));
};


const value: WatchUIContextValues = {
hangWatch: props.hangWatch,
watchStatus,
Expand All @@ -78,6 +101,9 @@ export default function WatchUIContextProvider(props: WatchUIContextProviderProp
buffering,
latency,
setLatencyValue,
availableRenditions,
activeRendition,
setActiveRendition: setActiveRenditionValue,
};

createEffect(() => {
Expand Down Expand Up @@ -133,6 +159,25 @@ export default function WatchUIContextProvider(props: WatchUIContextProviderProp
const latency = effect.get(watch.latency);
setLatency(latency);
});

watch.signals.effect((effect) => {
const rootCatalog = effect.get(watch.broadcast.catalog);
const videoCatalog = rootCatalog?.video;
const renditions = videoCatalog?.renditions ?? {};

const renditionsList: Rendition[] = Object.entries(renditions).map(([name, config]) => ({
name,
width: (config as Catalog.VideoConfig).codedWidth,
height: (config as Catalog.VideoConfig).codedHeight,
}));

setAvailableRenditions(renditionsList);
});

watch.signals.effect((effect) => {
const selected = effect.get(watch.video.source.active);
setActiveRendition(selected);
});
});

return <WatchUIContext.Provider value={value}>{props.children}</WatchUIContext.Provider>;
Expand Down
43 changes: 43 additions & 0 deletions js/hang-ui/src/Components/watch/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,46 @@
border-radius: 8px;
margin: 8px 0px;
}

.qualitySelectorContainer {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: transparent;
border-radius: 8px;
margin: 8px 0;
}

.qualityLabel {
font-size: 20px;
font-weight: 500;
color: #fff;
white-space: nowrap;
}

.qualitySelect {
flex: 0 1 auto;
font-size: 16px;
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: background 0.2s ease;
}

.qualitySelect:hover {
background: rgba(255, 255, 255, 0.15);
}

.qualitySelect:focus {
outline: 2px solid rgba(255, 255, 255, 0.3);
outline-offset: 2px;
}

.qualitySelect option {
background: #1a1a1a;
color: #fff;
}
6 changes: 5 additions & 1 deletion js/hang/src/watch/video/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export type Target = {
// The desired size of the video in pixels.
pixels?: number;

// Optional manual override for the selected rendition name.
rendition?: string;

// TODO bitrate
};

Expand Down Expand Up @@ -139,7 +142,8 @@ export class Source {
const supported = effect.get(this.#supported);
const target = effect.get(this.target);

const selected = this.#selectRendition(supported, target);
const manual = target?.rendition;
const selected = manual && manual in supported ? manual : this.#selectRendition(supported, target);
if (!selected) return;

effect.set(this.#selected, selected);
Expand Down
Loading