Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8bf3569
added cmaf support con relay
Dec 23, 2025
58c1ffe
MSE support
Jan 2, 2026
30139cd
adjusting logs and coments
Jan 6, 2026
deec2a9
Just comands updated flags
Jan 7, 2026
6ed6e21
catalog on source-mse
Jan 8, 2026
834d778
group logic MSE
Jan 8, 2026
89d510b
merging recent changes, solve conflicts
Jan 8, 2026
75d85df
rust and typescript fixes
Jan 8, 2026
f14e058
Update rs/hang-cli/src/publish.rs
jpbusta10 Jan 9, 2026
807db9e
cargo formatting issue
Jan 9, 2026
326cafe
audio sync issues
Jan 9, 2026
a73fd9c
coments cleanup
Jan 9, 2026
ec4a1a0
variable names: legacy->native fmp4->cmaf
Jan 9, 2026
3b20c43
optiona setAudioSync
Jan 9, 2026
8cbbf40
init segmet on catalog - timestamp fix - AV sync on MSE
Jan 15, 2026
3fa2bc8
base64 to cargo
Jan 15, 2026
3857fb2
fix minor issues - aboid playback by group
Jan 15, 2026
006ed39
fix pause audio bug
Jan 19, 2026
ab0e9f7
mp4 specific mime
Jan 19, 2026
3639400
JS side feels good.
kixelated Jan 21, 2026
f9114fa
ugh kinda works.
kixelated Jan 22, 2026
39a44fc
Merge remote-tracking branch 'origin/main' into full-cmaf-import-v2
kixelated Jan 22, 2026
1fa9ad4
More fixes n stuff I don't remember.
kixelated Jan 22, 2026
cacab58
Pre-PR changes.
kixelated Jan 22, 2026
d2814a3
WIP
kixelated Jan 24, 2026
fa79b92
Add min_buffer so MSE playback is decent.
kixelated Jan 24, 2026
f73752b
Merge remote-tracking branch 'origin/main' into full-cmaf-import-v2
kixelated Jan 24, 2026
a69b830
Add a vibe-coded buffer visualization.
kixelated Jan 26, 2026
01dd90e
Fix some MSE bugs.
kixelated Jan 26, 2026
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions js/hang-demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@

The broadcast path is overwritten by the ?path query parameter in index.ts.

TODO: There's a bug with Big Buck Bunny causing audio to stutter, so we need to increase the latency to 100ms.

NOTE: `reload` will detect when the broadcast goes offline/online and automatically reconnect.
TODO: Cloudflare doesn't support it yet (SUBSCRIBE_NAMESPACE), so make sure you remove it if you're using Cloudflare.

NOTE: You can use a <video> element instead of a <canvas> element to use the MSE backend.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would put this comment above the video tag.

This is not as well tested as has higher latency, but will support more devices.
-->
<hang-watch-ui>
<hang-watch id="watch" url="%VITE_RELAY_URL%" path="bbb" muted latency="100" reload>
<hang-watch id="watch" url="%VITE_RELAY_URL%" path="bbb" muted buffer="100" reload>
<canvas style="width: 100%; height: auto;"></canvas>
<!-- <video style="width: 100%; height: auto;" autoplay muted></video> -->
</hang-watch>
</hang-watch-ui>

Expand All @@ -40,6 +42,7 @@

<h3>Other demos:</h3>
<ul>
<li><a href="mse.html">Watch a broadcast (MSE).</a></li>
<li><a href="publish.html">Publish a broadcast.</a></li>
<li><a href="meet.html">Watch a room of broadcasts.</a></li>
<li><a href="support.html">Check browser support.</a></li>
Expand Down Expand Up @@ -117,8 +120,8 @@ <h3>Tips:</h3>
</code></pre>
</p>
<p>
Using something more niche? There's also a <code
class="language-typescript">subscribe()</code> method to trigger a callback on change.
Using something more niche? There's also a <code class="language-typescript">subscribe()</code> method to
trigger a callback on change.

<pre><code class="language-typescript">
const cleanup = hang.volume.subscribe((volume) => {
Expand Down
5 changes: 3 additions & 2 deletions js/hang-demo/src/meet.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@

<h3>Other demos:</h3>
<ul>
<li><a href="index.html">Watch a single broadcast.</a></li>
<li><a href="publish.html">Publish a single broadcast.</a></li>
<li><a href="index.html">Watch a broadcast (WebCodecs).</a></li>
<li><a href="mse.html">Watch a broadcast (MSE).</a></li>
<li><a href="publish.html">Publish a broadcast.</a></li>
<li><a href="support.html">Check browser support.</a></li>
</ul>

Expand Down
64 changes: 64 additions & 0 deletions js/hang-demo/src/mse.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="en" class="dark">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MoQ Demo (MSE)</title>

<link rel="stylesheet" href="index.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>

<body class="dark:bg-black p-4">
<!-- Show if this browser supports everything we need. -->
<hang-support mode="watch" show="partial"></hang-support>

<!--
This demo uses MSE (Media Source Extensions) instead of WebCodecs.
MSE has broader device support but higher latency compared to the WebCodecs canvas renderer.

The relay url is loaded from an environment as it contains a generated JWT authentication token.
Feel free to hard-code it if you have public access configured, like `url="https://cdn.moq.dev/anon"`
NOTE: `http` performs an insecure certificate check. You must use `https` in production.

The broadcast path is overwritten by the ?path query parameter in index.ts.

NOTE: `reload` will detect when the broadcast goes offline/online and automatically reconnect.
TODO: Cloudflare doesn't support it yet (SUBSCRIBE_NAMESPACE), so make sure you remove it if you're using Cloudflare.
-->
<hang-watch-ui>
<hang-watch id="watch" url="%VITE_RELAY_URL%" path="bbb" muted buffer="150" reload>
<video style="width: 100%; height: auto;" autoplay muted></video>
</hang-watch>
</hang-watch-ui>

<!-- Configure the relay URL and broadcast name. Auto-discovers available broadcasts. -->
<hang-config id="config" url="%VITE_RELAY_URL%" path="bbb"></hang-config>

<h3>Other demos:</h3>
<ul>
<li><a href="index.html">Watch a broadcast (WebCodecs).</a></li>
<li><a href="publish.html">Publish a broadcast.</a></li>
<li><a href="meet.html">Watch a room of broadcasts.</a></li>
<li><a href="support.html">Check browser support.</a></li>
</ul>

<h3>Tips:</h3>
<p>
This demo uses <a href="https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API">MSE (Media
Source Extensions)</a>
via a <code>&lt;video&gt;</code> element instead of WebCodecs via <code>&lt;canvas&gt;</code>.
MSE has broader device support but higher latency.
</p>
<p>
To use MSE, provide a <code>&lt;video&gt;</code> element instead of a <code>&lt;canvas&gt;</code> element:
<pre><code class="language-html">&lt;hang-watch url="https://cdn.moq.dev/anon" path="bbb"&gt;
&lt;video style="max-width: 100%; height: auto;" autoplay muted&gt;&lt;/video&gt;
&lt;/hang-watch&gt;</code></pre>
</p>
</body>

<script type="module" src="index.ts"></script>

</html>
1 change: 1 addition & 0 deletions js/hang-demo/src/publish.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ <h3>Other demos:</h3>
<ul>
<li><a href="index.html?path=me" target="_blank" rel="noreferrer" id="watch">Watch your broadcast: <span
id="watch-name"></span></a></li>
<li><a href="mse.html">Watch a broadcast (MSE).</a></li>
<li><a href="meet.html">Watch a room of broadcasts.</a></li>
<li><a href="support.html">Check browser support.</a></li>
</ul>
Expand Down
3 changes: 2 additions & 1 deletion js/hang-demo/src/support.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

<h3>Other demos:</h3>
<ul>
<li><a href="index.html">Watch a single broadcast.</a></li>
<li><a href="index.html">Watch a broadcast (WebCodecs).</a></li>
<li><a href="mse.html">Watch a broadcast (MSE).</a></li>
<li><a href="publish.html">Publish a broadcast.</a></li>
<li><a href="meet.html">Watch a room of broadcasts.</a></li>
</ul>
Expand Down
10 changes: 4 additions & 6 deletions js/hang-ui/src/shared/components/stats/providers/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,11 @@ export class AudioProvider extends BaseProvider {
return;
}

const active = this.props.audio.source.active.peek();
const rendition = this.props.audio.rendition.peek();
const config = this.props.audio.config.peek();
const stats = this.props.audio.stats.peek();

const config = this.props.audio.source.config.peek();

const stats = this.props.audio.source.stats.peek();

if (!active || !config) {
if (!rendition || !config) {
this.context.setDisplayData("N/A");
return;
}
Expand Down
18 changes: 2 additions & 16 deletions js/hang-ui/src/shared/components/stats/providers/buffer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Getter } from "@moq/signals";
import type { BufferStatus, ProviderContext, SyncStatus } from "../types";
import type { ProviderContext } from "../types";
import { BaseProvider } from "./base";

/**
Expand All @@ -22,21 +22,7 @@ export class BufferProvider extends BaseProvider {
}

this.signals.effect((effect) => {
const syncStatus = effect.get(video.source.syncStatus as Getter<SyncStatus | undefined>);
const bufferStatus = effect.get(video.source.bufferStatus as Getter<BufferStatus | undefined>);
const latency = effect.get(video.source.latency as Getter<number | undefined>);

const isLatencyValid = latency !== null && latency !== undefined && latency > 0;
const bufferPercentage =
syncStatus?.state === "wait" && syncStatus?.bufferDuration !== undefined && isLatencyValid
? Math.min(100, Math.round((syncStatus.bufferDuration / latency) * 100))
: bufferStatus?.state === "filled"
? 100
: 0;

const parts = [`${bufferPercentage}%`, isLatencyValid ? `${latency}ms` : "N/A"];

this.context?.setDisplayData(parts.join("\n"));
this.context?.setDisplayData("TODO");
});
}
}
6 changes: 3 additions & 3 deletions js/hang-ui/src/shared/components/stats/providers/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export class VideoProvider extends BaseProvider {
return;
}

const display = this.props.video.source.display.peek();
const stats = this.props.video.source.stats.peek();
const catalog = this.props.video.catalog.peek();
const stats = this.props.video.stats.peek();
const now = performance.now();

// Calculate FPS from frame count delta and timestamp delta
Expand Down Expand Up @@ -90,7 +90,7 @@ export class VideoProvider extends BaseProvider {
this.previousWhen = now;
}

const { width, height } = display ?? {};
const { width, height } = catalog?.display ?? {};

const parts = [
width && height ? `${width}x${height}` : "N/A",
Expand Down
92 changes: 13 additions & 79 deletions js/hang-ui/src/shared/components/stats/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export type KnownStatsProviders = "network" | "video" | "audio" | "buffer";

import type * as Hang from "@moq/hang";
import type * as Moq from "@moq/lite";

/**
* Context passed to providers for updating display data
*/
Expand All @@ -15,84 +18,15 @@ export interface VideoResolution {
height: number;
}

/**
* Stream sync status with buffer information
*/
export interface SyncStatus {
state: "ready" | "wait";
bufferDuration?: number;
}

/**
* Stream buffer fill status
*/
export interface BufferStatus {
state: "empty" | "filled";
}

/**
* Generic reactive signal interface for accessing stream data
*/
export interface Signal<T> {
peek(): T | undefined;
changed?(callback: (value: T | undefined) => void): () => void;
subscribe?(callback: () => void): () => void;
}

/**
* Audio stream statistics
*/
export type AudioStats = {
bytesReceived: number;
};

/**
* Audio stream source with reactive properties
*/
export interface AudioSource {
source: {
active: Signal<string>;
config: Signal<AudioConfig>;
stats: Signal<AudioStats>;
};
}

/**
* Audio stream configuration properties
*/
export interface AudioConfig {
sampleRate: number;
numberOfChannels: number;
bitrate?: number;
codec: string;
}
// TODO Don't re-export these types?
export type Signal<T> = Moq.Signals.Getter<T>;
export type AudioStats = Hang.Watch.Audio.Stats;
export type AudioSource = Hang.Watch.Audio.Backend;
export type AudioConfig = Hang.Catalog.AudioConfig;
export type VideoStats = Hang.Watch.Video.Stats;

/**
* Video stream statistics
*/
export type VideoStats = {
frameCount: number;
timestamp: number;
bytesReceived: number;
// TODO use Hang.Watch.Backend instead?
export type ProviderProps = {
audio: Hang.Watch.Audio.Backend;
video: Hang.Watch.Video.Backend;
};

/**
* Video stream source with reactive properties
*/
export interface VideoSource {
source: {
display: Signal<VideoResolution>;
syncStatus: Signal<SyncStatus>;
bufferStatus: Signal<BufferStatus>;
latency: Signal<number>;
stats: Signal<VideoStats>;
};
}

/**
* Props passed to metric providers containing stream sources
*/
export interface ProviderProps {
audio?: AudioSource;
video?: VideoSource;
}
Loading