@stinkycomputing/web-live-player 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ let t, o = !1, u = !1, g = 50, p = 0;
2
+ self.onmessage = async (a) => {
3
+ const { type: s, data: e } = a.data;
4
+ switch (s) {
5
+ case "init":
6
+ h(e.config), e.audioLevels && (u = !0, g = e.audioLevels.interval || 50);
7
+ break;
8
+ case "stream":
9
+ e.readable && await b(e.readable);
10
+ break;
11
+ case "encode":
12
+ y(e.frame);
13
+ break;
14
+ case "close":
15
+ v();
16
+ break;
17
+ default:
18
+ console.error("Unknown message type:", s);
19
+ }
20
+ };
21
+ function h(a) {
22
+ t = new AudioEncoder({
23
+ output: (s, e) => {
24
+ self.postMessage({
25
+ type: "chunk",
26
+ data: s,
27
+ metadata: e
28
+ });
29
+ },
30
+ error: (s) => {
31
+ self.postMessage({ type: "error", data: s.message });
32
+ }
33
+ }), t.configure(a), self.postMessage({ type: "ready" });
34
+ }
35
+ function y(a) {
36
+ if (!a) {
37
+ self.postMessage({ type: "error", data: "Received null or undefined frame" });
38
+ return;
39
+ }
40
+ if (!t) {
41
+ self.postMessage({ type: "error", data: "Encoder not initialized" });
42
+ return;
43
+ }
44
+ if (t.state !== "configured") {
45
+ self.postMessage({ type: "error", data: `Encoder not in configured state: ${t.state}` });
46
+ return;
47
+ }
48
+ try {
49
+ t.encode(a), a.close();
50
+ } catch (s) {
51
+ self.postMessage({ type: "error", data: s instanceof Error ? s.message : String(s) });
52
+ }
53
+ }
54
+ function v() {
55
+ if (t) {
56
+ try {
57
+ t.close();
58
+ } catch {
59
+ }
60
+ t = void 0;
61
+ }
62
+ o = !1, self.postMessage({ type: "closed" });
63
+ }
64
+ function M(a) {
65
+ const s = performance.now();
66
+ if (!(s - p < g)) {
67
+ p = s;
68
+ try {
69
+ const e = a.numberOfChannels || 2, r = new Float32Array(a.allocationSize({ planeIndex: 0 }) / 4);
70
+ a.copyTo(r, { planeIndex: 0 });
71
+ const c = [], i = r.length / e;
72
+ for (let n = 0; n < e; n++) {
73
+ let d = 0;
74
+ for (let l = 0; l < i; l++) {
75
+ const f = r[l * e + n];
76
+ d += f * f;
77
+ }
78
+ const m = Math.sqrt(d / i);
79
+ c[n] = m;
80
+ }
81
+ self.postMessage({
82
+ type: "audio-levels",
83
+ data: {
84
+ levels: c,
85
+ timestamp: s
86
+ }
87
+ });
88
+ } catch {
89
+ }
90
+ }
91
+ }
92
+ async function b(a) {
93
+ if (o) {
94
+ self.postMessage({ type: "error", data: "Already processing a stream" });
95
+ return;
96
+ }
97
+ o = !0;
98
+ const s = a.getReader();
99
+ try {
100
+ for (; ; ) {
101
+ const { done: e, value: r } = await s.read();
102
+ if (e)
103
+ break;
104
+ r && (u && M(r), y(r));
105
+ }
106
+ } catch (e) {
107
+ self.postMessage({
108
+ type: "error",
109
+ data: e instanceof Error ? e.message : String(e)
110
+ });
111
+ } finally {
112
+ s.releaseLock(), o = !1, self.postMessage({ type: "stream-complete" });
113
+ }
114
+ }
@@ -0,0 +1,91 @@
1
+ let s, t = !1, n = !1, o = 0, i = 60;
2
+ self.onmessage = async (r) => {
3
+ const { type: e, data: a } = r.data;
4
+ switch (e) {
5
+ case "init":
6
+ i = a.config.gopSize || 60, l(a.config);
7
+ break;
8
+ case "stream":
9
+ a.readable && await p(a.readable);
10
+ break;
11
+ case "encode":
12
+ f(a.frame);
13
+ break;
14
+ case "close":
15
+ d();
16
+ break;
17
+ case "request-keyframe":
18
+ n = !0;
19
+ break;
20
+ default:
21
+ console.error("Unknown message type:", e);
22
+ }
23
+ };
24
+ function l(r) {
25
+ s = new VideoEncoder({
26
+ output: (e, a) => {
27
+ self.postMessage({
28
+ type: "chunk",
29
+ data: e,
30
+ metadata: a
31
+ });
32
+ },
33
+ error: (e) => {
34
+ self.postMessage({ type: "error", data: e.message });
35
+ }
36
+ }), s.configure(r), self.postMessage({ type: "ready" });
37
+ }
38
+ function f(r) {
39
+ if (!r) {
40
+ self.postMessage({ type: "error", data: "Received null or undefined frame" });
41
+ return;
42
+ }
43
+ if (!s) {
44
+ self.postMessage({ type: "error", data: "Encoder not initialized" });
45
+ return;
46
+ }
47
+ if (s.state !== "configured") {
48
+ self.postMessage({ type: "error", data: `Encoder not in configured state: ${s.state}` });
49
+ return;
50
+ }
51
+ try {
52
+ o++;
53
+ let e = o % i === 0;
54
+ n && (n = !1, e = !0, o = 0), s.encode(r, { keyFrame: e }), r.close();
55
+ } catch (e) {
56
+ self.postMessage({ type: "error", data: e instanceof Error ? e.message : String(e) });
57
+ }
58
+ }
59
+ function d() {
60
+ if (s) {
61
+ try {
62
+ s.close();
63
+ } catch {
64
+ }
65
+ s = void 0;
66
+ }
67
+ t = !1, o = 0, self.postMessage({ type: "closed" });
68
+ }
69
+ async function p(r) {
70
+ if (t) {
71
+ self.postMessage({ type: "error", data: "Already processing a stream" });
72
+ return;
73
+ }
74
+ t = !0;
75
+ const e = r.getReader();
76
+ try {
77
+ for (; ; ) {
78
+ const { done: a, value: c } = await e.read();
79
+ if (a)
80
+ break;
81
+ c && f(c);
82
+ }
83
+ } catch (a) {
84
+ self.postMessage({
85
+ type: "error",
86
+ data: a instanceof Error ? a.message : String(a)
87
+ });
88
+ } finally {
89
+ e.releaseLock(), t = !1, self.postMessage({ type: "stream-complete" });
90
+ }
91
+ }
@@ -1,4 +1,4 @@
1
- import { HeaderCodecData } from '../protocol/sesame-binary-protocol';
1
+ import { IMediaCodecData } from '@stinkycomputing/sesame-api-client';
2
2
 
3
3
  export interface LiveAudioConfig {
4
4
  bufferDelayMs?: number;
@@ -16,7 +16,7 @@ export declare class LiveAudioPlayer {
16
16
  /**
17
17
  * Initialize with codec info from stream header
18
18
  */
19
- init(codecData?: HeaderCodecData): Promise<void>;
19
+ init(codecData?: IMediaCodecData): Promise<void>;
20
20
  /**
21
21
  * Build decoder config based on codec type
22
22
  */
@@ -32,7 +32,9 @@ export declare class LiveAudioPlayer {
32
32
  /**
33
33
  * Queue encoded audio data for decoding
34
34
  */
35
- decode(data: Uint8Array, pts?: bigint): void;
35
+ decode(data: Uint8Array, pts?: number | bigint | null | {
36
+ toNumber(): number;
37
+ }): void;
36
38
  /**
37
39
  * Set buffer delay for A/V sync
38
40
  */
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Audio Encoder Worker
3
+ *
4
+ * Handles audio encoding in a separate thread using WebCodecs AudioEncoder.
5
+ * Supports audio level monitoring for VU meters.
6
+ */
7
+ export {};
@@ -0,0 +1,105 @@
1
+ import { sesame } from '@stinkycomputing/sesame-api-client';
2
+
3
+ /**
4
+ * Serialized packet ready for transmission
5
+ */
6
+ export interface SerializedPacket {
7
+ data: ArrayBuffer;
8
+ isKeyframe: boolean;
9
+ timestamp: number;
10
+ type: 'video' | 'audio';
11
+ }
12
+ /**
13
+ * Interface for capture sinks that handle outgoing media data
14
+ */
15
+ export interface ICaptureSink {
16
+ /**
17
+ * Whether the sink is currently connected
18
+ */
19
+ readonly connected: boolean;
20
+ /**
21
+ * Connect to the transport
22
+ */
23
+ connect(): Promise<void>;
24
+ /**
25
+ * Disconnect from the transport
26
+ */
27
+ disconnect(): Promise<void>;
28
+ /**
29
+ * Reconnect to the transport
30
+ */
31
+ reconnect(): Promise<void>;
32
+ /**
33
+ * Send encoded media data
34
+ * @param packet The serialized packet to send
35
+ */
36
+ send(packet: SerializedPacket): void;
37
+ /**
38
+ * Set callback for keyframe requests from receiver
39
+ */
40
+ onKeyframeRequest(callback: () => void): void;
41
+ /**
42
+ * Dispose of all resources
43
+ */
44
+ dispose(): void;
45
+ }
46
+ /**
47
+ * Configuration for video stream in a capture sink
48
+ */
49
+ export interface VideoStreamConfig {
50
+ /** Video codec type */
51
+ codec: sesame.v1.wire.CodecType;
52
+ /** Video width */
53
+ width: number;
54
+ /** Video height */
55
+ height: number;
56
+ /** Timebase numerator */
57
+ timebaseNum?: number;
58
+ /** Timebase denominator */
59
+ timebaseDen?: number;
60
+ }
61
+ /**
62
+ * Configuration for audio stream in a capture sink
63
+ */
64
+ export interface AudioStreamConfig {
65
+ /** Audio codec type */
66
+ codec: sesame.v1.wire.CodecType;
67
+ /** Sample rate in Hz */
68
+ sampleRate: number;
69
+ /** Number of channels */
70
+ channels: number;
71
+ /** Timebase numerator */
72
+ timebaseNum?: number;
73
+ /** Timebase denominator */
74
+ timebaseDen?: number;
75
+ }
76
+ /**
77
+ * Base configuration for capture sinks
78
+ */
79
+ export interface CaptureSinkConfig {
80
+ /** Video stream configuration (optional if not sending video) */
81
+ video?: VideoStreamConfig;
82
+ /** Audio stream configuration (optional if not sending audio) */
83
+ audio?: AudioStreamConfig;
84
+ /** Topic/channel identifier for routing */
85
+ topic?: string;
86
+ /** Audio timestamp offset to align audio and video (in microseconds) */
87
+ audioTimestampOffset?: number;
88
+ }
89
+ /**
90
+ * Base class for capture sink implementations
91
+ */
92
+ export declare abstract class BaseCaptureSink implements ICaptureSink {
93
+ protected config: CaptureSinkConfig;
94
+ protected keyframeCallback?: () => void;
95
+ protected _connected: boolean;
96
+ constructor(config: CaptureSinkConfig);
97
+ get connected(): boolean;
98
+ abstract connect(): Promise<void>;
99
+ abstract disconnect(): Promise<void>;
100
+ abstract send(packet: SerializedPacket): void;
101
+ reconnect(): Promise<void>;
102
+ onKeyframeRequest(callback: () => void): void;
103
+ protected requestKeyframe(): void;
104
+ dispose(): void;
105
+ }
@@ -0,0 +1,120 @@
1
+ import { CodecType, sesame } from '@stinkycomputing/sesame-api-client';
2
+
3
+ export { CodecType };
4
+ /**
5
+ * Video codec configuration options
6
+ */
7
+ export interface VideoEncoderOptions {
8
+ codec: sesame.v1.wire.CodecType;
9
+ width?: number;
10
+ height?: number;
11
+ bitrate?: number;
12
+ frameRate?: number;
13
+ keyFrameInterval?: number;
14
+ latencyMode?: 'quality' | 'realtime';
15
+ }
16
+ /**
17
+ * Audio codec configuration options
18
+ */
19
+ export interface AudioEncoderOptions {
20
+ codec: sesame.v1.wire.CodecType;
21
+ sampleRate?: number;
22
+ channels?: number;
23
+ bitrate?: number;
24
+ latencyMode?: 'quality' | 'realtime';
25
+ }
26
+ /**
27
+ * Media capture configuration
28
+ */
29
+ export interface CaptureConfig {
30
+ /** Enable video capture */
31
+ video?: boolean | MediaTrackConstraints;
32
+ /** Enable audio capture */
33
+ audio?: boolean | MediaTrackConstraints;
34
+ /** Video encoder options */
35
+ videoEncoder?: VideoEncoderOptions;
36
+ /** Audio encoder options */
37
+ audioEncoder?: AudioEncoderOptions;
38
+ /** Enable audio level monitoring */
39
+ audioLevelMonitoring?: boolean;
40
+ /** Audio level reporting interval in ms */
41
+ audioLevelInterval?: number;
42
+ }
43
+ /**
44
+ * Default capture configuration
45
+ */
46
+ export declare const DEFAULT_CAPTURE_CONFIG: Required<CaptureConfig>;
47
+ /**
48
+ * Encoded chunk event from encoder
49
+ */
50
+ export interface EncodedChunkEvent {
51
+ type: 'audio' | 'video';
52
+ chunk: EncodedAudioChunk | EncodedVideoChunk;
53
+ keyframe: boolean;
54
+ timestamp: number;
55
+ metadata?: EncodedChunkMetadata;
56
+ }
57
+ /**
58
+ * Metadata associated with an encoded chunk
59
+ */
60
+ export interface EncodedChunkMetadata {
61
+ width?: number;
62
+ height?: number;
63
+ channels?: number;
64
+ sampleRate?: number;
65
+ decoderConfig?: VideoDecoderConfig | AudioDecoderConfig;
66
+ }
67
+ /**
68
+ * Audio level data event
69
+ */
70
+ export interface AudioLevelEvent {
71
+ timestamp: number;
72
+ levels: number[];
73
+ }
74
+ /**
75
+ * Capture statistics
76
+ */
77
+ export interface CaptureStats {
78
+ /** Number of video frames encoded */
79
+ videoFramesEncoded: number;
80
+ /** Number of audio frames encoded */
81
+ audioFramesEncoded: number;
82
+ /** Total bytes sent */
83
+ bytesSent: number;
84
+ /** Packets sent */
85
+ packetsSent: number;
86
+ /** Current video bitrate */
87
+ videoBitrate: number;
88
+ /** Current audio bitrate */
89
+ audioBitrate: number;
90
+ /** Capture start time */
91
+ startTime: number;
92
+ /** Duration in ms */
93
+ duration: number;
94
+ }
95
+ /**
96
+ * Capture state
97
+ */
98
+ export type CaptureState = 'idle' | 'initializing' | 'capturing' | 'paused' | 'stopped' | 'error';
99
+ /**
100
+ * Events emitted by the capture system
101
+ */
102
+ export interface CaptureEvents {
103
+ 'state-change': (state: CaptureState) => void;
104
+ 'encoded-chunk': (event: EncodedChunkEvent) => void;
105
+ 'audio-levels': (event: AudioLevelEvent) => void;
106
+ 'stats': (stats: CaptureStats) => void;
107
+ 'error': (error: Error) => void;
108
+ }
109
+ /**
110
+ * Convert CodecType to WebCodecs codec string
111
+ */
112
+ export declare function codecTypeToString(codec: sesame.v1.wire.CodecType): string;
113
+ /**
114
+ * Check if a codec is a video codec
115
+ */
116
+ export declare function isVideoCodec(codec: sesame.v1.wire.CodecType): boolean;
117
+ /**
118
+ * Check if a codec is an audio codec
119
+ */
120
+ export declare function isAudioCodec(codec: sesame.v1.wire.CodecType): boolean;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Capture Module
3
+ *
4
+ * Provides media capture capabilities for video and audio streaming.
5
+ * Supports encoding with WebCodecs and transport over MoQ or WebSocket.
6
+ */
7
+ export * from './capture-types';
8
+ export { MediaStreamEncoder } from './media-encoder';
9
+ export type { EncoderEventHandler } from './media-encoder';
10
+ export { BaseCaptureSink } from './capture-sink';
11
+ export type { ICaptureSink, CaptureSinkConfig, SerializedPacket, VideoStreamConfig, AudioStreamConfig, } from './capture-sink';
12
+ export { WebSocketCaptureSink, createWebSocketSink } from './websocket-sink';
13
+ export type { WebSocketSinkConfig } from './websocket-sink';
14
+ export { MoQCaptureSink, createMoQSink } from './moq-sink';
15
+ export type { MoQSinkConfig, MoQTrackConfig } from './moq-sink';
16
+ export { MediaCapture, createMediaCapture } from './media-capture';
17
+ export type { MediaCaptureConfig, CaptureEventHandler } from './media-capture';
@@ -0,0 +1,95 @@
1
+ import { CaptureConfig, CaptureState, CaptureStats, AudioLevelEvent } from './capture-types';
2
+ import { ICaptureSink } from './capture-sink';
3
+
4
+ /**
5
+ * Full media capture configuration
6
+ */
7
+ export interface MediaCaptureConfig extends CaptureConfig {
8
+ /** Capture sink for sending encoded data */
9
+ sink: ICaptureSink;
10
+ /** Topic/channel identifier for routing packets */
11
+ topic?: string;
12
+ /** Audio timestamp offset to align with video (microseconds) */
13
+ audioTimestampOffset?: number;
14
+ }
15
+ /**
16
+ * Event handler types
17
+ */
18
+ export type CaptureEventHandler<T> = (event: T) => void;
19
+ /**
20
+ * Media Capture - captures and encodes media from browser devices
21
+ */
22
+ export declare class MediaCapture {
23
+ private config;
24
+ private sink;
25
+ private encoder?;
26
+ private mediaStream?;
27
+ private state;
28
+ private disposed;
29
+ private stats;
30
+ private videoMetadata?;
31
+ private audioMetadata?;
32
+ private handlers;
33
+ constructor(config: MediaCaptureConfig);
34
+ /**
35
+ * Register event handler
36
+ */
37
+ on(event: 'state-change', handler: CaptureEventHandler<CaptureState>): this;
38
+ on(event: 'audio-levels', handler: CaptureEventHandler<AudioLevelEvent>): this;
39
+ on(event: 'stats', handler: CaptureEventHandler<CaptureStats>): this;
40
+ on(event: 'error', handler: CaptureEventHandler<Error>): this;
41
+ /**
42
+ * Unregister event handler
43
+ */
44
+ off(event: string, handler: CaptureEventHandler<any>): this;
45
+ private emit;
46
+ private setState;
47
+ /**
48
+ * Get current capture state
49
+ */
50
+ getState(): CaptureState;
51
+ /**
52
+ * Get current capture statistics
53
+ */
54
+ getStats(): CaptureStats;
55
+ /**
56
+ * Check if media devices are available
57
+ */
58
+ static hasMediaDevices(): boolean;
59
+ /**
60
+ * Get list of available media devices
61
+ */
62
+ static getDevices(): Promise<MediaDeviceInfo[]>;
63
+ /**
64
+ * Start capturing and encoding media
65
+ */
66
+ start(): Promise<void>;
67
+ /**
68
+ * Stop capturing
69
+ */
70
+ stop(): Promise<void>;
71
+ /**
72
+ * Request immediate keyframe
73
+ */
74
+ requestKeyframe(): void;
75
+ /**
76
+ * Get the underlying MediaStream (for preview purposes)
77
+ */
78
+ getMediaStream(): MediaStream | undefined;
79
+ /**
80
+ * Set an external MoQ session on the sink (if it's a MoQCaptureSink)
81
+ * Allows injecting an existing MoqSessionBroadcaster instance.
82
+ * @param session - MoqSessionBroadcaster instance to use for broadcasting
83
+ */
84
+ setMoQSession(session: any): void;
85
+ private handleEncodedChunk;
86
+ private createPacket;
87
+ /**
88
+ * Dispose of all resources
89
+ */
90
+ dispose(): void;
91
+ }
92
+ /**
93
+ * Factory function to create a media capture instance
94
+ */
95
+ export declare function createMediaCapture(config: MediaCaptureConfig): MediaCapture;
@@ -0,0 +1,65 @@
1
+ import { AudioEncoderOptions, VideoEncoderOptions, EncodedChunkEvent, AudioLevelEvent } from './capture-types';
2
+
3
+ /**
4
+ * Event handler types
5
+ */
6
+ export type EncoderEventHandler<T> = (event: T) => void;
7
+ /**
8
+ * Media stream encoder that handles both audio and video encoding
9
+ */
10
+ export declare class MediaStreamEncoder {
11
+ private audioWorker;
12
+ private videoWorker;
13
+ private audioProcessor;
14
+ private videoProcessor;
15
+ private disposed;
16
+ private handlers;
17
+ private audioReady;
18
+ private videoReady;
19
+ private hasAudio;
20
+ private hasVideo;
21
+ private audioMetadata?;
22
+ private videoMetadata?;
23
+ constructor(stream: MediaStream, videoOptions?: VideoEncoderOptions, audioOptions?: AudioEncoderOptions, audioLevelConfig?: {
24
+ enabled: boolean;
25
+ interval?: number;
26
+ });
27
+ /**
28
+ * Register event handler
29
+ */
30
+ on(event: 'chunk', handler: EncoderEventHandler<EncodedChunkEvent>): this;
31
+ on(event: 'audio-levels', handler: EncoderEventHandler<AudioLevelEvent>): this;
32
+ on(event: 'error', handler: EncoderEventHandler<Error>): this;
33
+ on(event: 'ready', handler: EncoderEventHandler<void>): this;
34
+ /**
35
+ * Unregister event handler
36
+ */
37
+ off(event: 'chunk', handler: EncoderEventHandler<EncodedChunkEvent>): this;
38
+ off(event: 'audio-levels', handler: EncoderEventHandler<AudioLevelEvent>): this;
39
+ off(event: 'error', handler: EncoderEventHandler<Error>): this;
40
+ off(event: 'ready', handler: EncoderEventHandler<void>): this;
41
+ private emit;
42
+ private checkReady;
43
+ private createAudioEncoder;
44
+ private createVideoEncoder;
45
+ private startProcessingAudio;
46
+ private startProcessingVideo;
47
+ /**
48
+ * Request an immediate keyframe from the video encoder
49
+ */
50
+ requestKeyframe(): void;
51
+ /**
52
+ * Dispose of all resources
53
+ */
54
+ dispose(): void;
55
+ }
56
+ declare global {
57
+ interface MediaStreamTrackProcessor<T> {
58
+ readable: ReadableStream<T>;
59
+ }
60
+ const MediaStreamTrackProcessor: {
61
+ new <T>(options: {
62
+ track: MediaStreamTrack;
63
+ }): MediaStreamTrackProcessor<T>;
64
+ };
65
+ }