Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
4c66b2b
Skip real audio frames replaced by silence.
Nov 7, 2025
5d87ade
Add timestamp correction
Nov 11, 2025
f03b692
Fix timestamp adjustment
Nov 12, 2025
069bcfa
Complete Timestamp manager
Nov 12, 2025
753c3ff
Integrate and finalize Timestamp manager
Nov 13, 2025
c31ac60
Add seek method draft
Nov 13, 2025
a907f3a
Improve start
Nov 14, 2025
b659346
Add sliding window calculations
Nov 17, 2025
f1ebfbe
Improve latency controller
Nov 18, 2025
1d8deaa
Add dynamic rate change for latency adjustment
Nov 19, 2025
5b0b473
Adjust latency controller parameters
Nov 20, 2025
4679198
Fix initial seek on startup. Adjust readable audio buffer step if the…
Nov 24, 2025
237a3e3
Fix test
Nov 24, 2025
0637688
Fix inspection faults
Nov 26, 2025
37eadbe
Start implementing WSOLA algorithm
Nov 27, 2025
00b5147
Merge branch 'main' into fast_forward
Dec 2, 2025
a80dab3
Implement wsola as shared buffer preprocessor
Dec 3, 2025
ab5fa26
Continue wsola implementation
Dec 4, 2025
26daedb
wsola implementation 12/06
Dec 8, 2025
3d6fcc3
wsola implementation 12/09
Dec 10, 2025
58f5d2a
wsola implementation 12/12
Dec 15, 2025
576f9fc
Debug sample addressing issues
Dec 17, 2025
d2d2a65
Fix sample addressing bugs
Dec 18, 2025
e4c1188
Investigate WSOLA quality problem
Dec 22, 2025
e69e739
Update WSOLA algorithm
Dec 23, 2025
fc2c728
Debug noise
Dec 24, 2025
70e6e9c
Fix excessive read on fast forward
Dec 25, 2025
1ce8efc
Fix read count error
Dec 26, 2025
870e27a
Optimize overlap method. Calculate wsola parameters for specific rate
Dec 29, 2025
da78461
Finalize WSOLA calculations
Dec 31, 2025
a8f1cab
Add deferred execution for shared buffer. Fix silence insertion sampl…
Jan 6, 2026
10307a1
Fix audio buffer overflow behavior. Remove debug code.
Jan 8, 2026
f9fada8
Fix tests
Jan 8, 2026
02e3d89
Prettify
Jan 8, 2026
5950555
Merge branch 'main' into fast_forward
Jan 9, 2026
5657e13
Merge related fixes
Jan 9, 2026
2987a4e
prettify
Jan 9, 2026
f44b491
Revert index page
Jan 9, 2026
7f28694
Add transferrable audio buffers to avoid extra frame copying. Move po…
Jan 14, 2026
184088a
Fix transferrable buffers overflow case
Jan 15, 2026
b1e4d9e
Fix inspection faults
Jan 15, 2026
1178da0
Fix overflow frames release
Jan 16, 2026
cc15ee1
Merge branch 'fast_forward' into transferrable_sab
Jan 16, 2026
b7d089c
Fix reset. Group buffer updates.
Jan 16, 2026
a71005b
Prettify
Jan 16, 2026
377a35d
Merge branch 'main' into reconnect
Jan 19, 2026
a12f46d
Adding auto reconnect
Jan 19, 2026
ad54f71
Complete reconnect functionality. Fix eventBus instance retrieval in …
Jan 20, 2026
f3028d8
Add detailed error log for unsynced renditions. Prettify.
Jan 20, 2026
3371172
Add syncBuffer setting
Jan 21, 2026
be58652
Send streams in the connection-established event in the documented way
Jan 21, 2026
f8d0b9a
Handle sync mode parameters
Jan 21, 2026
144cbde
Merge branch 'reconnect' into sync_mode
Jan 22, 2026
56ac045
Rename params
Jan 22, 2026
c168e40
Pass all offsets to latency controller instances
Jan 27, 2026
26787ec
Debug sync time offset
Jan 27, 2026
fb7879e
Wrapping up
Jan 28, 2026
a5bd9f7
Adjust sync mode behavior
Jan 29, 2026
cbb7759
Fix sync
Feb 1, 2026
44b7a32
Slight tuning
Feb 2, 2026
91f3fb1
Fix ensureCapacity behavior for empty buffer
Feb 3, 2026
5646c2d
Fix audio config update. Fix new decoder creation
Feb 3, 2026
d69e90b
Debug and fix init switch issues
Feb 4, 2026
33a7631
Merge branch 'main' into advertizer
Feb 9, 2026
c9768c0
Add init segment switch timestamp reset handling
Feb 9, 2026
376d4e5
Perform seek on init segment switch
Feb 10, 2026
8c0c5d6
Add AdvertizerEvaluator. Fix a/v de-sync after continuous switches.
Feb 11, 2026
b4eebac
Finalize advertizer
Feb 12, 2026
43ff039
Debug init segment switch glitch
Feb 13, 2026
2bb28c8
Continue debugging
Feb 14, 2026
5ba4d1c
Fix wrong timestamps on init switch ts reset
Feb 16, 2026
c3622a2
Remove debug logging and fine tune params. Edit README
Feb 17, 2026
b69a5b8
Fix tests. Prettify
Feb 17, 2026
8fe48fe
Merge branch 'main' into advertizer
Feb 19, 2026
afd1287
Merge branch 'main' into advertizer
Feb 20, 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
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,16 +278,12 @@ The following features are planned for upcoming releases:

- Automatic aspect ratio detection
- Picture-in-Picture (PiP)
- Latency retention for asynchronous renditions
- CEA-608 closed captions
- VOD playback (DVR support)
- VOD thumbnail previews
- SEI timecodes support
- WebTransport protocol
- Nimble Advertizer integration
- Sync mode
- Screenshot capture
- Splash/startup image
- Extended Player API
- OffscreenCanvas rendering
- Resume from pause in DVR mode (no auto-jump to live)
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
},
workletLogs: true,
latencyAdjustMethod: "fast-forward", // "fast-forward" | "seek"
syncBuffer: 3000,
});

window.nimio.on("nimio:play", (inst, contId) => {
Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"devDependencies": {
"@vitest/coverage-v8": "^3.2.4",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"prettier": "^3.8.1",
"rollup-plugin-copy": "^3.5.0",
"tseep": "^1.3.1",
"vite": "^6.4.1",
Expand Down
144 changes: 144 additions & 0 deletions src/advertizer/evaluator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { LoggersFactory } from "@/shared/logger";

export class AdvertizerEvaluator {
#types = [];
#confIvalUs = 2_000_000;

constructor(instName, port) {
this._tracks = {};
this._switches = {};
this._swCnt = 0;

if (port) {
port.addEventListener("message", this._portMessageHandler.bind(this));
port.postMessage("transp-discont-eval-ready");
} else {
this._pendingActions = [];
}

this._logger = LoggersFactory.create(instName, "Advertizer Eval", port);
}

reset() {
this.clearPendingActions();
}

isApplicable() {
return this._swCnt > 0;
}

computeShift(curTsUs, availUs) {
// skip for a while if a track is being switched
if (this._tracks.video === null || this._tracks.audio === null) return 0;

let res = 0;
let preMatches = [];
let win = [Infinity, 0];
let del = {};
for (let j = 0; j < this.#types.length; j++) {
let trackId = this._tracks[this.#types[j]];
let switches = this._switches[trackId];
if (!switches) break;

for (let i = 0; i < switches.length; i++) {
if (switches[i].to < curTsUs) {
del[trackId] = i;
continue;
}
if (switches[i].from - curTsUs > this.#confIvalUs) {
break;
}
if (win[0] > switches[i].from || win[0] < curTsUs) {
win[0] = switches[i].from;
}
if (win[1] < switches[i].to) win[1] = switches[i].to;
preMatches.push(i);
}
}
if (preMatches.length === this.#types.length && win[0] - curTsUs < 3_000) {
let delta = win[1] - curTsUs;
// this._logger.debug(`Switch delta is ${delta / 1000}ms, curTs = ${curTsUs/1000}, to = ${win[1] / 1000}, from = ${win[0] / 1000}`);
if (delta + this._bufToKeep <= availUs) {
for (let j = 0; j < this.#types.length; j++) {
let switches = this._switches[this._tracks[this.#types[j]]];
switches.splice(0, preMatches[j] + 1);
this._logger.debug(
`Remove ${this.#types[j]} switches till ${preMatches[j] + 1}. Cur ts = ${curTsUs}. Left ${switches.length}`,
);
}
del = null;
res = delta;
this._logger.debug(
`Computed init switch shift ${res / 1000}ms to ${win[1]}`,
);
}
}
if (del) {
for (let tId in del) {
this._switches[tId].splice(0, del[tId] + 1);
this._logger.debug(
`Remove ${tId} switches till ${del[tId] + 1}. Cur ts = ${curTsUs}. Left ${this._switches[tId].length}`,
);
}
}

return res;
}

handleAction(data) {
switch (data.op) {
case "init-switch":
if (!this._switches[data.id]) {
this._switches[data.id] = [];
}
this._switches[data.id].push({
from: data.data.fromPtsUs,
to: data.data.toPtsUs,
});
this._swCnt++;
break;
case "main":
this._tracks[data.type] = data.id;
if (!this.#types.includes(data.type)) {
this.#types.push(data.type);
}
break;
case "rem":
if (this._switches[data.id]) {
this._swCnt -= this._switches[data.id].length;
}
this._switches[data.id] = undefined;
if (this._tracks[data.type] === data.id) {
this._tracks[data.type] = null;
}
break;
default:
this._logger.error(`Unknown action ${data.op}`);
break;
}
}

hasPendingActions() {
return !!this._pendingActions && this._pendingActions.length > 0;
}

clearPendingActions() {
if (this._pendingActions) this._pendingActions.length = 0;
}

get pendingActions() {
return this._pendingActions;
}

set bufferToKeep(valUs) {
this._bufToKeep = valUs;
}

_portMessageHandler(event) {
const msg = event.data;
if (!msg || msg.aux) return;
if (msg.type === "transp-track-action") {
this.handleAction(msg.data);
}
}
}
8 changes: 4 additions & 4 deletions src/audio/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class AudioConfig {
this._numberOfChannels = config.numberOfChannels;
this._sampleCount = config.sampleCount;

return config;
return this;
}

smpCntToTsUs(smpCnt) {
Expand All @@ -34,9 +34,9 @@ export class AudioConfig {
}

tsUsToSmpCnt(tsUs) {
let smpCnt = (tsUs / 1000) * (this._sampleRate / 1000);
if (smpCnt < 0) smpCnt = 0;
return (smpCnt + 0.5) >>> 0;
let k = tsUs > 0 ? 1 : -1;
let smpCnt = ((k * tsUs) / 1000) * (this._sampleRate / 1000);
return k * ((smpCnt + 0.5) >>> 0);
}

isCompatible(config) {
Expand Down
16 changes: 11 additions & 5 deletions src/audio/nimio-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { AudioConfig } from "./config";
import { LoggersFactory } from "@/shared/logger";
import { LatencyController } from "@/latency-controller";
import { WsolaProcessor } from "@/media/processors/wsola-processor";
import { AdvertizerEvaluator } from "@/advertizer/evaluator";

class AudioNimioProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);

this.port.start();
LoggersFactory.setLevel(options.processorOptions.logLevel);
LoggersFactory.toggleWorkletLogs(options.processorOptions.enableLogs);
this._logger = LoggersFactory.create(
Expand All @@ -32,19 +34,26 @@ class AudioNimioProcessor extends AudioWorkletProcessor {
this._sampleCount,
);

this._advertizerEval = new AdvertizerEvaluator(
options.processorOptions.instanceName,
this.port,
);

this._idle = options.processorOptions.idle;
this._targetLatencyMs = options.processorOptions.latency;
this._latencyCtrl = new LatencyController(
options.processorOptions.instanceName,
this._stateManager,
this._audioConfig,
this._advertizerEval,
{
latency: this._targetLatencyMs,
tolerance: options.processorOptions.latencyTolerance,
adjustMethod: options.processorOptions.latencyAdjustMethod,
video: options.processorOptions.videoEnabled,
audio: !this._idle,
port: this.port,
syncBuffer: options.processorOptions.syncBuffer,
},
);
this._latencyCtrl.speedFn = this._setSpeed.bind(this);
Expand Down Expand Up @@ -86,17 +95,14 @@ class AudioNimioProcessor extends AudioWorkletProcessor {
}

if (!this._idle) {
this._stateManager.incSilenceUs(this._samplesDurationUs(sampleCount));
let durUs = (this._audioConfig.smpCntToTsUs(sampleCount) + 0.5) >>> 0;
this._stateManager.incSilenceUs(durUs);
if (!this._audioBuffer.isShareable) {
this._audioBuffer.ensureCapacity();
}
}
}

_samplesDurationUs(sampleCount) {
return (this._audioConfig.smpCntToTsUs(sampleCount) + 0.5) >>> 0;
}

_setSpeed(speed, availableMs) {
if (this._speed === speed) return;
this._speed = speed;
Expand Down
2 changes: 1 addition & 1 deletion src/audio/volume-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AudioVolumeController {

this._storageId = settings.volumeId;
this._lastVolume = this._getStoredVolume();
if (settings.muted) {
if (settings.muted || this._muted) {
this._gainer.gain.value = 0;
this._muted = true;
this._eventBus.emit("nimio:muted", true);
Expand Down
Loading