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
160 changes: 160 additions & 0 deletions lib/moq-publisher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// src/components/publisher-moq.ts

import STYLE_SHEET from "./publisher-moq.css"
import { PublisherApi, PublisherOptions } from "../publish"

export class PublisherMoq extends HTMLElement {
private shadow: ShadowRoot
private cameraSelect!: HTMLSelectElement
private microphoneSelect!: HTMLSelectElement
private previewVideo!: HTMLVideoElement
private connectButton!: HTMLButtonElement
private mediaStream: MediaStream | null = null

private publisher?: PublisherApi
private isPublishing = false

constructor() {
super()
this.shadow = this.attachShadow({ mode: "open" })

// CSS
const style = document.createElement("style")
style.textContent = STYLE_SHEET
this.shadow.appendChild(style)

const container = document.createElement("div")
container.classList.add("publisher-container")

this.cameraSelect = document.createElement("select")
this.microphoneSelect = document.createElement("select")
this.previewVideo = document.createElement("video")
this.connectButton = document.createElement("button")

this.previewVideo.autoplay = true
this.previewVideo.playsInline = true
this.previewVideo.muted = true
this.connectButton.textContent = "Connect"

container.append(this.cameraSelect, this.microphoneSelect, this.previewVideo, this.connectButton)
this.shadow.appendChild(container)

// Bindings
this.handleDeviceChange = this.handleDeviceChange.bind(this)
this.handleClick = this.handleClick.bind(this)

// Listeners
navigator.mediaDevices.addEventListener("devicechange", this.handleDeviceChange)

Check failure on line 47 in lib/moq-publisher/index.ts

View workflow job for this annotation

GitHub Actions / check

Promise returned in function argument where a void return was expected
this.cameraSelect.addEventListener("change", () => this.startPreview())
this.microphoneSelect.addEventListener("change", () => this.startPreview())
this.connectButton.addEventListener("click", this.handleClick)
}

connectedCallback() {
this.populateDeviceLists()
}

disconnectedCallback() {
navigator.mediaDevices.removeEventListener("devicechange", this.handleDeviceChange)
}

private async handleDeviceChange() {
await this.populateDeviceLists()
}

private async populateDeviceLists() {
const devices = await navigator.mediaDevices.enumerateDevices()
const vids = devices.filter((d) => d.kind === "videoinput")
const mics = devices.filter((d) => d.kind === "audioinput")

this.cameraSelect.innerHTML = ""
this.microphoneSelect.innerHTML = ""

vids.forEach((d) => {
const o = document.createElement("option")
o.value = d.deviceId
o.textContent = d.label || `Camera ${this.cameraSelect.length + 1}`
this.cameraSelect.append(o)
})
mics.forEach((d) => {
const o = document.createElement("option")
o.value = d.deviceId
o.textContent = d.label || `Mic ${this.microphoneSelect.length + 1}`
this.microphoneSelect.append(o)
})

await this.startPreview()
}

private async startPreview() {
const vidId = this.cameraSelect.value
const micId = this.microphoneSelect.value
if (this.mediaStream) {
this.mediaStream.getTracks().forEach((t) => t.stop())
}
this.mediaStream = await navigator.mediaDevices.getUserMedia({
video: vidId ? { deviceId: { exact: vidId } } : true,
audio: micId ? { deviceId: { exact: micId } } : true,
})

this.previewVideo.srcObject = this.mediaStream
}

private async handleClick() {
if (!this.isPublishing) {
if (!this.mediaStream) {
console.warn("No media stream available")
return
}

const audioTrack = this.mediaStream!.getAudioTracks()[0];
const settings = audioTrack.getSettings();

const sampleRate = settings.sampleRate ?? (await new AudioContext()).sampleRate;
const numberOfChannels = settings.channelCount ?? 2;

const videoConfig: VideoEncoderConfig = {codec: "avc1.42E01E", width: this.previewVideo.videoWidth, height: this.previewVideo.videoHeight, bitrate:1000000, framerate: 60};
const audioConfig: AudioEncoderConfig = {codec: "opus", sampleRate, numberOfChannels, bitrate:64000};


const opts: PublisherOptions = {
url: this.getAttribute("src")!,
fingerprintUrl: this.getAttribute("fingerprint")!,
namespace: [
this.getAttribute("namespace")! || crypto.randomUUID()
],
media: this.mediaStream,
video: videoConfig,
audio: audioConfig,
}

console.log("Publisher Options", opts)

this.publisher = new PublisherApi(opts)

try {
await this.publisher.publish()
this.isPublishing = true
this.connectButton.textContent = "Stop"
this.cameraSelect.disabled = true
this.microphoneSelect.disabled = true
} catch (err) {
console.error("Publish failed:", err)
}
} else {
try {
await this.publisher!.stop()
} catch (err) {
console.error("Stop failed:", err)
} finally {
this.isPublishing = false
this.connectButton.textContent = "Connect"
this.cameraSelect.disabled = false
this.microphoneSelect.disabled = false
}
}
}
}

customElements.define("publisher-moq", PublisherMoq)
export default PublisherMoq
17 changes: 17 additions & 0 deletions lib/moq-publisher/publisher-moq.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.publisher-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

#cameraSelect,
#microphoneSelect,
#connect {
font-size: 1rem;
padding: 0.5rem;
}

#preview {
background: black;
object-fit: cover;
}
24 changes: 21 additions & 3 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@
"description": "Media over QUIC library",
"license": "(MIT OR Apache-2.0)",
"wc-player": "video-moq/index.ts",
"wc-publisher": "moq-publisher/index.ts",
"simple-player": "playback/index.ts",
"files": ["dist"],
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/moq-player.esm.js",
"require": "./dist/moq-player.cjs.js"
},
"./moq-publisher": {
"import": "./dist/moq-publisher.esm.js",
"require": "./dist/moq-publisher.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-publisher": "dist/moq-publisher.iife.js",
"iife-simple": "dist/moq-simple-player.iife.js",
"types": "dist/types/moq-player.d.ts",
"scripts": {
Expand Down Expand Up @@ -58,8 +66,18 @@
"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"]
"production": [
"chrome >= 97",
"edge >= 98",
"firefox >= 130",
"opera >= 83",
"safari >= 18"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"repository": {
"type": "git",
Expand Down
56 changes: 56 additions & 0 deletions lib/publish/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// publisher-api.ts
import { Client } from "../transport/client"
import { Broadcast, BroadcastConfig } from "../contribute"
import { Connection } from "../transport/connection"

export interface PublisherOptions {
url: string
namespace: string[]
media: MediaStream
video?: VideoEncoderConfig
audio?: AudioEncoderConfig
fingerprintUrl?: string
}

export class PublisherApi {
private client: Client
private connection?: Connection
private broadcast?: Broadcast
private opts: PublisherOptions

constructor(opts: PublisherOptions) {
this.opts = opts
this.client = new Client({
url: opts.url,
fingerprint: opts.fingerprintUrl,
role: "publisher",
})
}

async publish(): Promise<void> {
if (!this.connection) {
this.connection = await this.client.connect()
}

const bcConfig: BroadcastConfig = {
connection: this.connection,
namespace: this.opts.namespace,
media: this.opts.media,
video: this.opts.video,
audio: this.opts.audio,
}

this.broadcast = new Broadcast(bcConfig)
}

async stop(): Promise<void> {
if (this.broadcast) {
this.broadcast.close()
await this.broadcast.closed()
}
if (this.connection) {
this.connection.close()
await this.connection.closed()
}
}
}
17 changes: 17 additions & 0 deletions lib/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ const basePlugins = [
]

module.exports = [
{
input: pkg["wc-publisher"],
output: [
{
file: pkg["iife-publisher"],
format: "iife",
name: "MoqPublisher",
sourcemap: true,
},
{
file: pkg.exports["./moq-publisher"].import,
format: "esm",
sourcemap: true,
},
],
plugins: [...basePlugins, css()],
},
{
input: pkg["wc-player"],
output: [
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

5 changes: 5 additions & 0 deletions samples/publisher/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<body>
<script src="/moq-player/moq-publisher.iife.js"></script>
<publisher-moq src="https://localhost:4443" namespace="bbb" fingerprint="https://localhost:4443/fingerprint">
</publisher-moq>
</body>
Loading