-
Notifications
You must be signed in to change notification settings - Fork 136
Cmaf Native Support #822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Cmaf Native Support #822
Changes from all commits
8bf3569
58c1ffe
30139cd
deec2a9
6ed6e21
834d778
89d510b
75d85df
f14e058
807db9e
326cafe
a73fd9c
ec4a1a0
3b20c43
8cbbf40
3fa2bc8
3857fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,12 +11,14 @@ import type * as Time from "../time"; | |
| */ | ||
| export function encodeTimestamp(timestamp: Time.Micro, container: Catalog.Container = DEFAULT_CONTAINER): Uint8Array { | ||
| switch (container) { | ||
| case "legacy": | ||
| case "native": | ||
| return encodeVarInt(timestamp); | ||
| case "raw": | ||
| return encodeU64(timestamp); | ||
| case "fmp4": | ||
| throw new Error("fmp4 container not yet implemented"); | ||
| case "cmaf": { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't be encoding the timestamp here. The entire frame payload should be a fMP4 fragment. |
||
| // CMAF fragments contain timestamps in moof atoms, no header needed | ||
| return new Uint8Array(0); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -32,16 +34,17 @@ export function decodeTimestamp( | |
| container: Catalog.Container = DEFAULT_CONTAINER, | ||
| ): [Time.Micro, Uint8Array] { | ||
| switch (container) { | ||
| case "legacy": { | ||
| case "native": { | ||
| const [value, remaining] = decodeVarInt(buffer); | ||
| return [value as Time.Micro, remaining]; | ||
| } | ||
| case "raw": { | ||
| const [value, remaining] = decodeU64(buffer); | ||
| return [value as Time.Micro, remaining]; | ||
| } | ||
| case "fmp4": | ||
| throw new Error("fmp4 container not yet implemented"); | ||
| case "cmaf": { | ||
| return [0 as Time.Micro, buffer]; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -54,12 +57,12 @@ export function decodeTimestamp( | |
| */ | ||
| export function getTimestampSize(container: Catalog.Container = DEFAULT_CONTAINER): number { | ||
| switch (container) { | ||
| case "legacy": | ||
| case "native": | ||
| return 8; // VarInt maximum size | ||
| case "raw": | ||
| return 8; // u64 fixed size | ||
| case "fmp4": | ||
| throw new Error("fmp4 container not yet implemented"); | ||
| case "cmaf": | ||
| return 8; // VarInt maximum size (same as native) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type * as Catalog from "../catalog"; | ||
|
|
||
| /** | ||
| * Builds a MIME type string for MediaSource from a codec string. | ||
| * | ||
| * @param codec - The codec string from the catalog (e.g., "avc1.42E01E", "mp4a.40.2") | ||
| * @param type - "video" or "audio" | ||
| * @returns MIME type string (e.g., "video/mp4; codecs=\"avc1.42E01E\"") | ||
| */ | ||
| export function buildMimeType(codec: string, type: "video" | "audio"): string { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is very MP4 specific, but it has a generic name. |
||
| // For MP4 containers, we use the standard MIME type format | ||
| // Most codecs are already in the correct format for MSE | ||
| return `${type}/mp4; codecs="${codec}"`; | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a MIME type is supported by MediaSource. | ||
| * | ||
| * @param mimeType - The MIME type to check | ||
| * @returns true if supported, false otherwise | ||
| */ | ||
| export function isMimeTypeSupported(mimeType: string): boolean { | ||
| return MediaSource.isTypeSupported(mimeType); | ||
| } | ||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's locate the file and understand its context
find . -type f -name "mime.ts" | head -20Repository: moq-dev/moq Length of output: 81 🏁 Script executed: # Search for imports of isMimeTypeSupported to see where it's used
rg "isMimeTypeSupported" --type ts --type js -B 2 -A 2Repository: moq-dev/moq Length of output: 1004 🏁 Script executed: # Check if this module is imported in test files or SSR contexts
rg "from.*mime|import.*mime" --type ts --type js | grep -E "(test|spec|ssr|server)" | head -20Repository: moq-dev/moq Length of output: 37 🏁 Script executed: # Check the overall structure of the mime.ts file
cat -n js/hang/src/util/mime.tsRepository: moq-dev/moq Length of output: 2076 🏁 Script executed: # Search for other uses of MediaSource in the codebase
rg "MediaSource" --type ts --type js -B 1 -A 1Repository: moq-dev/moq Length of output: 5428 🏁 Script executed: # Check if mime.ts or any functions from it are imported in test files
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.js" -o -name "*.spec.js" \) | xargs grep -l "mime" 2>/dev/null || echo "No test files found importing mime utilities"Repository: moq-dev/moq Length of output: 99 🏁 Script executed: # Check the project structure to understand if this is browser-only or if it can be run in Node
head -30 package.jsonRepository: moq-dev/moq Length of output: 442 🏁 Script executed: # Look for any SSR or Node-related imports or configurations
find . -name "*.config.*" -o -name "tsconfig*.json" | head -10Repository: moq-dev/moq Length of output: 335 🏁 Script executed: # Check if there are any existing guards for browser APIs in the codebase
rg "typeof.*!==.*undefined|globalThis\.|typeof.*global" --type ts --type js | head -20Repository: moq-dev/moq Length of output: 2011 🏁 Script executed: # Check where buildVideoMimeType and buildAudioMimeType are used
rg "buildVideoMimeType|buildAudioMimeType" --type ts --type js -B 2 -A 2Repository: moq-dev/moq Length of output: 1585 🏁 Script executed: # Check if mime.ts is a browser-only utility or if it might be loaded in Node contexts
find . -type f \( -name "package.json" -o -name "tsconfig.json" \) -path "*/js/hang/*" | xargs cat | grep -E "target|module|browser|exports" | head -20Repository: moq-dev/moq Length of output: 87 🏁 Script executed: # Check the vite and vitest configs to understand the build target
cat js/hang-demo/vite.config.tsRepository: moq-dev/moq Length of output: 724 🏁 Script executed: # Double-check if there's explicit browser/non-browser context configuration
cat js/hang/package.json | head -50Repository: moq-dev/moq Length of output: 1428 Guard The function directly accesses Proposed fix export function isMimeTypeSupported(mimeType: string): boolean {
- return MediaSource.isTypeSupported(mimeType);
+ return typeof MediaSource !== "undefined" && MediaSource.isTypeSupported(mimeType);
}🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Builds and validates a MIME type for video from catalog config. | ||
| * | ||
| * @param config - Video configuration from catalog | ||
| * @returns MIME type string or undefined if not supported | ||
| */ | ||
| export function buildVideoMimeType(config: Catalog.VideoConfig): string | undefined { | ||
| const mimeType = buildMimeType(config.codec, "video"); | ||
| if (isMimeTypeSupported(mimeType)) { | ||
| return mimeType; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Builds and validates a MIME type for audio from catalog config. | ||
| * | ||
| * @param config - Audio configuration from catalog | ||
| * @returns MIME type string or undefined if not supported | ||
| */ | ||
| export function buildAudioMimeType(config: Catalog.AudioConfig): string | undefined { | ||
| const mimeType = buildMimeType(config.codec, "audio"); | ||
| if (isMimeTypeSupported(mimeType)) { | ||
| return mimeType; | ||
| } | ||
| return undefined; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,7 +46,8 @@ export class Emitter { | |
| }); | ||
|
|
||
| this.#signals.effect((effect) => { | ||
| const enabled = !effect.get(this.paused) && !effect.get(this.muted); | ||
| const paused = effect.get(this.paused); | ||
| const enabled = !paused; | ||
| this.source.enabled.set(enabled); | ||
| }); | ||
|
|
||
|
|
@@ -56,7 +57,44 @@ export class Emitter { | |
| this.muted.set(volume === 0); | ||
| }); | ||
|
|
||
| // Handle MSE path (HTMLAudioElement) vs WebCodecs path (AudioWorklet) | ||
| this.#signals.effect((effect) => { | ||
| const mseAudio = effect.get(this.source.mseAudioElement); | ||
| if (mseAudio) { | ||
| // MSE path: control HTMLAudioElement directly | ||
| effect.effect(() => { | ||
| const volume = effect.get(this.volume); | ||
| const muted = effect.get(this.muted); | ||
| const paused = effect.get(this.paused); | ||
| mseAudio.volume = volume; | ||
| mseAudio.muted = muted; | ||
|
|
||
| // Control play/pause state | ||
| if (paused && !mseAudio.paused) { | ||
| mseAudio.pause(); | ||
| } else if (!paused && mseAudio.paused) { | ||
| // Resume if paused - try to play even if readyState is low | ||
| const tryPlay = () => { | ||
| if (!paused && mseAudio.paused) { | ||
| mseAudio | ||
| .play() | ||
| .catch((err) => console.error("[Audio Emitter] Failed to resume audio:", err)); | ||
| } | ||
| }; | ||
|
|
||
| // Try to play if we have metadata (HAVE_METADATA = 1), browser will start when ready | ||
| if (mseAudio.readyState >= HTMLMediaElement.HAVE_METADATA) { | ||
| tryPlay(); | ||
| } else { | ||
| // Wait for loadedmetadata event if not ready yet | ||
| mseAudio.addEventListener("loadedmetadata", tryPlay, { once: true }); | ||
| } | ||
|
Comment on lines
+85
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential memory leak: event listener not cleaned up. When Consider cleaning up the event listener in the effect's cleanup phase: Proposed fix // Try to play if we have metadata (HAVE_METADATA = 1), browser will start when ready
if (mseAudio.readyState >= HTMLMediaElement.HAVE_METADATA) {
tryPlay();
} else {
// Wait for loadedmetadata event if not ready yet
mseAudio.addEventListener("loadedmetadata", tryPlay, { once: true });
+ effect.cleanup(() => mseAudio.removeEventListener("loadedmetadata", tryPlay));
}
🤖 Prompt for AI Agents |
||
| } | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // WebCodecs path: use AudioWorklet with GainNode | ||
| const root = effect.get(this.source.root); | ||
| if (!root) return; | ||
|
|
||
|
|
@@ -76,9 +114,10 @@ export class Emitter { | |
| }); | ||
| }); | ||
|
|
||
| // Only apply gain transitions for WebCodecs path (when gain node exists) | ||
| this.#signals.effect((effect) => { | ||
| const gain = effect.get(this.#gain); | ||
| if (!gain) return; | ||
| if (!gain) return; // MSE path doesn't use gain node | ||
|
|
||
| // Cancel any scheduled transitions on change. | ||
| effect.cleanup(() => gain.gain.cancelScheduledValues(gain.context.currentTime)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update stale comment about CMAF availability.
The comment indicates CMAF is "(future)", but the implementation in
js/hang/src/container/codec.ts(lines 18-21, 45-52) and usage in MSE source files show it's already active.📝 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents