Skip to content

Commit

Permalink
Merge with main: add dual builds (player without controls and web com…
Browse files Browse the repository at this point in the history
…ponent with controls) and new usage examples
  • Loading branch information
JoaquinBCh committed Jan 14, 2025
2 parents 18d9328 + 03d6329 commit 5c5c6eb
Show file tree
Hide file tree
Showing 30 changed files with 7,741 additions and 2,759 deletions.
1 change: 1 addition & 0 deletions lib/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
plugins: ["@typescript-eslint", "prettier", "solid"],
root: true,
env: {
commonjs: true,
browser: true,
es2022: true,
worker: true,
Expand Down
2 changes: 2 additions & 0 deletions lib/common/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class Watch<T> {

constructor(init: T) {
this.#next = new Deferred<WatchNext<T>>()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.#current = [init, this.#next.promise]
}

Expand All @@ -44,6 +45,7 @@ export class Watch<T> {
}

const next = new Deferred<WatchNext<T>>()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.#current = [v, next.promise]
this.#next.resolve(this.#current)
this.#next = next
Expand Down
2 changes: 1 addition & 1 deletion lib/contribute/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class Container {
this.#track = this.#mp4.addTrack(options)
if (!this.#track) throw new Error("failed to initialize MP4 track")

const buffer = MP4.ISOFile.writeInitializationSegment(this.#mp4.ftyp!, this.#mp4.moov!, 0, 0)
const buffer = MP4.ISOFile.writeInitializationSegment(this.#mp4.ftyp, this.#mp4.moov, 0, 0)
const data = new Uint8Array(buffer)

controller.enqueue({
Expand Down
4 changes: 2 additions & 2 deletions lib/media/mp4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ MP4.BoxParser.dOpsBox.prototype.write = function (stream: MP4.Stream) {
stream.writeUint8(this.ChannelMappingFamily)

if (this.ChannelMappingFamily !== 0) {
stream.writeUint8(this.StreamCount!)
stream.writeUint8(this.CoupledCount!)
stream.writeUint8(this.StreamCount)
stream.writeUint8(this.CoupledCount)
for (const mapping of this.ChannelMapping!) {
stream.writeUint8(mapping)
}
Expand Down
59 changes: 51 additions & 8 deletions lib/package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
{
"name": "@kixelated/moq",
"type": "module",
"version": "0.1.4",
"name": "moq-player",
"version": "0.0.1",
"description": "Media over QUIC library",
"license": "(MIT OR Apache-2.0)",
"repository": "github:kixelated/moq-js",
"wc-player": "video-moq/index.ts",
"simple-player": "playback/index.ts",
"exports": {
".": {
"import": "./dist/moq-player.esm.js",
"require": "./dist/moq-player.cjs.js"
},
"./simple-player": {
"import": "./dist/moq-simple-player.esm.js",
"require": "./dist/moq-simple-player.cjs.js"
}
},
"iife": "dist/moq-player.iife.js",
"iife-simple": "dist/moq-simple-player.iife.js",
"types": "dist/types/moq-player.d.ts",
"scripts": {
"build": "tsc -b && cp ../LICENSE* ./dist && cp ./README.md ./dist && cp ./package.json ./dist",
"lint": "eslint .",
"build": "rollup -c",
"dev": "rollup -c -w",
"lint": "eslint . --ext .js,.ts,.jsx,.tsx",
"fmt": "prettier --write ."
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.1",
"@types/audioworklet": "^0.0.50",
"@types/dom-mediacapture-transform": "^0.1.6",
"@types/dom-webcodecs": "^0.1.8",
"@typescript/lib-dom": "npm:@types/web@^0.0.115",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@typescript/lib-dom": "npm:@types/web@^0.0.115",
"cross-env": "^7.0.2",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.1",
"typescript": "^5.1.6"
"rollup": "^4.28.0",
"rollup-plugin-import-css": "^3.5.8",
"rollup-plugin-sourcemaps": "^0.6.2",
"rollup-plugin-web-worker-loader": "github:montevideo-tech/rollup-plugin-web-worker-loader",
"tslib": "^2.8.1",
"typescript": "^5.7.2"
},
"dependencies": {
"mp4box": "^0.5.2"
},
"browserslist": {
"production": [
"chrome >= 97",
"edge >= 98",
"firefox >= 130",
"opera >= 83",
"safari >= 18"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
20 changes: 11 additions & 9 deletions lib/playback/audio.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
/// <reference types="vite/client" />

import * as Message from "./worker/message"

// This is a non-standard way of importing worklet/workers.
// Unfortunately, it's the only option because of a Vite bug: https://github.com/vitejs/vite/issues/11823
import workletURL from "./worklet/index.ts?worker&url"
import registerMyAudioWorklet from "audio-worklet:./worklet/index.ts"

// NOTE: This must be on the main thread
export class Audio {
context: AudioContext
worklet: Promise<AudioWorkletNode>
volumeNode: GainNode

constructor(config: Message.ConfigAudio) {
this.context = new AudioContext({
latencyHint: "interactive",
sampleRate: config.sampleRate,
})
this.volumeNode = this.context.createGain()
this.volumeNode.gain.value = 1.0

this.worklet = this.load(config)
}

private async load(config: Message.ConfigAudio): Promise<AudioWorkletNode> {
// Load the worklet source code.
await this.context.audioWorklet.addModule(workletURL)

await registerMyAudioWorklet(this.context)
const volume = this.context.createGain()
volume.gain.value = 2.0

Expand All @@ -36,8 +34,8 @@ export class Audio {
}

// Connect the worklet to the volume node and then to the speakers
worklet.connect(volume)
volume.connect(this.context.destination)
worklet.connect(this.volumeNode)
this.volumeNode.connect(this.context.destination)

worklet.port.postMessage({ config })

Expand All @@ -47,4 +45,8 @@ export class Audio {
private on(_event: MessageEvent) {
// TODO
}

public setVolume(newVolume: number) {
this.volumeNode.gain.setTargetAtTime(newVolume, this.context.currentTime, 0.01)
}
}
19 changes: 13 additions & 6 deletions lib/playback/backend.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/// <reference types="vite/client" />

import * as Message from "./worker/message"
import { Audio } from "./audio"

import MediaWorker from "./worker?worker"
// import WebWorker from 'web-worker:./Worker.ts';
import MediaWorker from "web-worker:./worker/index.ts"

import { RingShared } from "../common/ring"
import { Root, isAudioTrack } from "../media/catalog"
import { GroupHeader } from "../transport/objects"
Expand All @@ -26,8 +26,7 @@ export default class Backend {

constructor(config: PlayerConfig) {
// TODO does this block the main thread? If so, make this async
// @ts-expect-error: The Vite typing is wrong https://github.com/vitejs/vite/blob/22bd67d70a1390daae19ca33d7de162140d533d6/packages/vite/client.d.ts#L182
this.#worker = new MediaWorker({ format: "es" })
this.#worker = new MediaWorker()
this.#worker.addEventListener("message", this.on.bind(this))

let sampleRate: number | undefined
Expand Down Expand Up @@ -68,7 +67,11 @@ export default class Backend {
}

pause() {
this.send({ pause: true })
this.send({ play: false })
}

play() {
this.send({ play: true })
}

async mute() {
Expand All @@ -87,6 +90,10 @@ export default class Backend {
this.send({ segment }, segment.stream)
}

setVolume(newVolume: number) {
this.#audio?.setVolume(newVolume)
}

async close() {
this.#worker.terminate()
await this.#audio?.context.close()
Expand Down
25 changes: 18 additions & 7 deletions lib/playback/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface PlayerConfig {
}

// This class must be created on the main thread due to AudioContext.
export class Player {
export default class Player {
#backend: Backend

// A periodically updated timeline
Expand Down Expand Up @@ -306,9 +306,9 @@ export class Player {
// Added this to divide play and pause into two different functions
async togglePlayPause() {
if (this.#paused) {
this.play()
await this.play()
} else {
this.pause()
await this.pause()
}
}

Expand All @@ -320,16 +320,27 @@ export class Player {
this.subscribeFromTrackName(this.#audioTrackName)
await this.#backend.unmute()
}
this.#backend.play()
}
}

async pause() {
if (!this.#paused) {
await this.unsubscribeFromTrack(this.#videoTrackName)
await this.unsubscribeFromTrack(this.#audioTrackName)
await this.#backend.mute()
this.#backend.pause()
this.#paused = true
const mutePromise = this.#backend.mute()
const audioPromise = this.unsubscribeFromTrack(this.#audioTrackName)
const videoPromise = this.unsubscribeFromTrack(this.#videoTrackName)
this.#backend.pause()
await Promise.all([mutePromise, audioPromise, videoPromise])
}
}

async setVolume(newVolume: number) {
this.#backend.setVolume(newVolume)
if (newVolume == 0 && !this.#muted) {
await this.mute(true)
} else if (newVolume > 0 && this.#muted) {
await this.mute(false)
}
}

Expand Down
17 changes: 13 additions & 4 deletions lib/playback/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Worker {

on(e: MessageEvent) {
const msg = e.data as Message.ToWorker
// console.log("message: ", msg)

if (msg.config) {
this.#onConfig(msg.config)
Expand All @@ -30,8 +31,10 @@ class Worker {
this.#onInit(msg.init)
} else if (msg.segment) {
this.#onSegment(msg.segment).catch(console.warn)
} else if (msg.pause) {
this.#onPause(msg.pause)
} else if (msg.play === false) {
this.#onPause(msg.play)
} else if (msg.play === true) {
this.#onPlay(msg.play)
} else {
throw new Error(`unknown message: + ${JSON.stringify(msg)}`)
}
Expand Down Expand Up @@ -103,11 +106,17 @@ class Worker {
await segment.close()
}

#onPause(pause: boolean) {
if (this.#video && pause) {
#onPause(play: boolean) {
if (this.#video && !play) {
this.#video.pause()
}
}

#onPlay(play: boolean) {
if (this.#video && play) {
this.#video.play()
}
}
}

// Pass all events to the worker
Expand Down
2 changes: 1 addition & 1 deletion lib/playback/worker/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export interface ToWorker {
// Sent on each init/data stream
init?: Init
segment?: Segment
pause?: boolean
play?: boolean

/*
// Sent to control playback
Expand Down
16 changes: 13 additions & 3 deletions lib/playback/worker/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ export class Renderer {

#decoderConfig?: DecoderConfig
#waitingForKeyframe: boolean = true
#paused: boolean

constructor(config: Message.ConfigVideo, timeline: Component) {
this.#canvas = config.canvas
this.#timeline = timeline
this.#paused = false

this.#queue = new TransformStream({
start: this.#start.bind(this),
Expand All @@ -41,14 +43,22 @@ export class Renderer {
}

pause() {
console.log("pause")
this.#paused = true
this.#decoder.flush().catch((err) => {
console.error(err)
})
this.#waitingForKeyframe = true
}

play() {
this.#paused = false
}

async #run() {
const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader()
for (;;) {
const { value: frame, done } = await reader.read()
if (this.#paused) continue
if (done) break

self.requestAnimationFrame(() => {
Expand All @@ -74,8 +84,8 @@ export class Renderer {
}

#transform(frame: Frame) {
if (this.#decoder.state === "closed") {
console.warn("Decoder is closed. Skipping frame.")
if (this.#decoder.state === "closed" || this.#paused) {
console.warn("Decoder is closed or paused. Skipping frame.")
return
}

Expand Down
Loading

0 comments on commit 5c5c6eb

Please sign in to comment.