Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 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
be58652
Send streams in the connection-established event in the documented way
Jan 21, 2026
bb4b6ea
Merge branch 'main' into reconnect
Feb 9, 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
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,10 @@ The following features are planned for upcoming releases:
- SEI timecodes support
- WebTransport protocol
- Nimble Advertizer integration
- Automatic reconnection
- Sync mode
- Screenshot capture
- Splash/startup image
- Extended Player API
- Muted autoplay
- Dynamic latency adjustment
- OffscreenCanvas rendering
- Resume from pause in DVR mode (no auto-jump to live)

Expand Down
8 changes: 6 additions & 2 deletions src/media/decoders/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,18 @@ export class DecoderFlow {

let srcFirstTsUs = this._switchPeerFlow.firstSwitchTsUs;
if (srcFirstTsUs !== null) {
if (Math.abs(frame.timestamp - srcFirstTsUs) >= SWITCH_THRESHOLD_US) {
let absDiff = Math.abs(frame.timestamp - srcFirstTsUs);
if (absDiff >= SWITCH_THRESHOLD_US) {
this._logger.debug(
`Handle dst switch frame - excessive diff dst ts: ${frame.timestamp}, src ts: ${srcFirstTsUs}`,
);
this._switchContext = null;
this._switchPeerFlow.destroy();
this._switchPeerFlow = null;
this._onSwitchResult(false);
this._onSwitchResult(
false,
`Rendition switch isn't possible, because timestamps of renditions aren't in sync: the gap is approximately ${(absDiff / 1_000_000).toFixed(1)} seconds. Please check your ABR setup.`,
);
return;
}

Expand Down
15 changes: 15 additions & 0 deletions src/nimio-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@ export const NimioTransport = {
audioSetup: this._onAudioSetupReceived.bind(this),
audioCodec: this._onAudioCodecDataReceived.bind(this),
audioChunk: this._onAudioChunkReceived.bind(this),
disconnect: this._onDisconnect.bind(this),
};
},

_onDisconnect(data) {
this._state.stop();
this._sldpManager.resetCurrentStreams();
if (this._isAutoAbr()) {
this._abrController.stop({ hard: true });
}
this._resetPlayback();
if (!this._reconnect.schedule(this._playCb)) {
this._logger.debug("Stop reconnecting");
return this._ui.drawPlay();
}
this._logger.debug("Attempt to reconnect");
},

_onVideoSetupReceived(data) {
if (!data || !data.config) {
this._setNoVideo();
Expand Down
119 changes: 68 additions & 51 deletions src/nimio.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { EventBus } from "./event-bus";
import { WorkletLogReceiver } from "./shared/worklet-log-receiver";
import { createSharedBuffer, isSharedBuffer } from "./shared/shared-buffer";
import { resolveContainer } from "./shared/container";
import { Reconnector } from "./reconnector";

let scriptPath;
if (document.currentScript === null) {
Expand Down Expand Up @@ -111,7 +112,7 @@ export default class Nimio {
dropZeroDurationFrames: this._config.dropZeroDurationFrames,
});

this._resetPlaybackTimstamps();
this._resetPlaybackTimestamps();
this._renderVideoFrame = this._renderVideoFrame.bind(this);
this._ctx = this._ui.canvas.getContext("2d");

Expand All @@ -120,6 +121,7 @@ export default class Nimio {
this._context = PlaybackContext.getInstance(this._instName);
this._sldpManager = new SLDPManager(this._instName);
this._sldpManager.init(this._transport, this._config);
this._reconnect = new Reconnector(this._instName, this._config.reconnects);

this._audioWorkletReady = null;
this._audioConfig = new AudioConfig(48000, 1, 1024); // default values
Expand All @@ -134,21 +136,20 @@ export default class Nimio {

this._createLatencyController();

this._playCb = this.play.bind(this);
if (this._config.autoplay) {
setTimeout(() => this.play(), 0);
setTimeout(this._playCb, 0);
setTimeout(() => {
this._ui.hideControls(true);
}, 1000);
}
}

play() {
const initialPlay = !this._state.isPaused();
if (this._state.isPlaying()) return;

if (this._pauseTimeoutId !== null) {
clearTimeout(this._pauseTimeoutId);
this._pauseTimeoutId = null;
}
const initialPlay = !this._state.isPaused();
this._cancelPauseTimeout();

this._state.start();
this._latencyCtrl.start();
Expand All @@ -172,8 +173,11 @@ export default class Nimio {
}

pause() {
if (this._state.isPaused()) return;

this._state.pause();
this._latencyCtrl.pause();
this._reconnect.stop();
if (this._isAutoAbr()) this._abrController.stop();
this._pauseTimeoutId = setTimeout(() => {
this._logger.debug("Auto stop");
Expand All @@ -183,53 +187,16 @@ export default class Nimio {

stop(closeConnection) {
this._state.stop();
this._sldpManager.stop({ closeConnection });
this._reconnect.reset();
if (this._debugView) this._debugView.stop();

if (this._isAutoAbr()) {
this._abrController.stop({ hard: true });
}

this._sldpManager.stop(!!closeConnection);
if (this._debugView) {
this._debugView.stop();
}

this._videoBuffer.reset();
this._noVideo = this._config.audioOnly;

this._stopAudio();
this._noAudio = this._config.videoOnly;
if (this._audioBuffer) {
this._audioBuffer.reset();
}
this._latencyCtrl.reset();

if (this._nextRenditionData) {
if (this._nextRenditionData.decoderFlow) {
this._nextRenditionData.decoderFlow.destroy();
}
this._nextRenditionData = null;
}

["video", "audio"].forEach((type) => {
if (this._decoderFlows[type]) {
this._decoderFlows[type].destroy();
this._decoderFlows[type] = null;
}
});

this._state.setPlaybackStartTsUs(0);
this._state.setVideoLatestTsUs(0);
this._state.setAudioLatestTsUs(0);
this._state.resetCurrentTsSmp();
this._resetPlaybackTimstamps();
this._resetPlayback();

this._ui.drawPlay();
this._ctx.clearRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height);
this._pauseTimeoutId = null;

this._vuMeterSvc.stop();
this._audioGraphCtrl.dismantle();
this._audioCtxProvider.reset();
this._workletLogReceiver.reset();
}

destroy() {
Expand Down Expand Up @@ -325,7 +292,10 @@ export default class Nimio {
? this._onVideoStartTsNotSet.bind(this)
: this._onAudioStartTsNotSet.bind(this);
decoderFlow.onDecodingError = this._onDecodingError.bind(this);
decoderFlow.onSwitchResult = (done) => {
decoderFlow.onSwitchResult = (done, msg) => {
if (msg && !done) {
this._logger.error(msg);
}
this._onRenditionSwitchResult(type, done);
};
decoderFlow.onInputCancel = () => {
Expand Down Expand Up @@ -435,7 +405,54 @@ export default class Nimio {
this._state.setPlaybackStartTsUs(this._playbackStartTsUs);
}

_resetPlaybackTimstamps() {
_cancelPauseTimeout() {
if (this._pauseTimeoutId === null) return;
clearTimeout(this._pauseTimeoutId);
this._pauseTimeoutId = null;
}

_resetPlayback() {
this._ctx.clearRect(0, 0, this._ctx.canvas.width, this._ctx.canvas.height);

this._videoBuffer.reset();
this._noVideo = this._config.audioOnly;

this._stopAudio();
this._noAudio = this._config.videoOnly;
if (this._audioBuffer) {
this._audioBuffer.reset();
}
this._latencyCtrl.reset();

if (this._nextRenditionData) {
if (this._nextRenditionData.decoderFlow) {
this._nextRenditionData.decoderFlow.destroy();
}
this._nextRenditionData = null;
}

["video", "audio"].forEach((type) => {
if (this._decoderFlows[type]) {
this._decoderFlows[type].destroy();
this._decoderFlows[type] = null;
}
});

this._state.setPlaybackStartTsUs(0);
this._state.setVideoLatestTsUs(0);
this._state.setAudioLatestTsUs(0);
this._state.resetCurrentTsSmp();
this._resetPlaybackTimestamps();

this._vuMeterSvc.stop();
this._audioGraphCtrl.dismantle();
this._audioCtxProvider.reset();

this._cancelPauseTimeout();
this._workletLogReceiver.reset();
}

_resetPlaybackTimestamps() {
this._playbackStartTsUs = 0;
this._firstAudioFrameTsUs = this._firstVideoFrameTsUs = 0;
}
Expand Down
23 changes: 23 additions & 0 deletions src/playback/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,29 @@ class PlaybackContext {
return this._curConf[type] && idx === this._curConf[type].idx;
}

getStreamsConfig() {
let res = [];
for (let i = 0; i < this._streams.length; i++) {
const streamInfo = this._streams[i].stream_info;
let strm = {
name: this._streams[i].stream,
bandwidth: streamInfo.bandwidth,
};
if (streamInfo.vcodec) {
strm.width = streamInfo.width;
strm.height = streamInfo.height;
strm.vcodec = streamInfo.vcodec;
strm.video = streamInfo.vcodecSupported ? "supported" : "not supported";
}
if (streamInfo.acodec) {
strm.acodec = streamInfo.acodec;
strm.audio = streamInfo.acodecSupported ? "supported" : "not supported";
}
res.push(strm);
}
return res;
}

get streams() {
return this._streams;
}
Expand Down
1 change: 1 addition & 0 deletions src/player-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const DEFAULTS = {
fullscreen: false,
dropZeroDurationFrames: false,
hardwareAcceleration: false,
reconnects: 10,
};

const REQUIRED_KEYS = ["streamUrl", "container"];
Expand Down
39 changes: 39 additions & 0 deletions src/reconnector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { EventBus } from "@/event-bus";

export class Reconnector {
constructor(instName, count) {
this._count = count;
this._eventBus = EventBus.getInstance(instName);
this._eventBus.on("nimio:connection-established", () => this.reset());
this.reset();
}

reset() {
this.stop();
this._done = 0;
}

stop() {
if (!this._timer) return;
clearTimeout(this._timer);
this._timer = undefined;
}

schedule(cb) {
if (this._done >= this._count) return false;

if (!this._timer) {
const inst = this;
this._timer = setTimeout(function () {
inst._done++;
cb();
inst._timer = undefined;
}, this._timeout());
}
return true;
}

_timeout() {
return this._done < 5 ? 1000 : 5000;
}
}
4 changes: 4 additions & 0 deletions src/shared/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function multiInstanceService(Klass) {
let instances = {};
return {
getInstance: function (instanceId) {
if (!instanceId) {
console.error("multiInstance getInstance is called without instanceId");
return null;
}
if (!instances[instanceId]) {
instances[instanceId] = new Klass(instanceId);
instances[instanceId].constructor = null;
Expand Down
18 changes: 14 additions & 4 deletions src/sldp/manager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlaybackContext } from "@/playback/context";
import { LoggersFactory } from "@/shared/logger";
import { EventBus } from "@/event-bus";

export class SLDPManager {
constructor(instName) {
Expand All @@ -11,6 +12,7 @@ export class SLDPManager {

this._context = PlaybackContext.getInstance(instName);
this._logger = LoggersFactory.create(instName, "SLDP Manager");
this._eventBus = EventBus.getInstance(instName);
// TODO: set from config.syncBuffer
this._useSteady = false;
}
Expand Down Expand Up @@ -42,19 +44,22 @@ export class SLDPManager {
});
}

stop(closeConnection) {
let sns = this._curStreams.map((s) => s.sn);
stop(opts = {}) {
this._transport.send("stop", {
close: !!closeConnection,
sns: sns,
close: !!opts.closeConnection,
sns: this.resetCurrentStreams(),
});
}

resetCurrentStreams() {
const sns = this._curStreams.map((s) => s.sn);
for (let i = 0; i < sns.length; i++) {
delete this._reqStreams[sns[i]];
}
this._curStreams = [];

this._transport.send("removeTimescale", sns);
return sns;
}

requestStream(type, idx, offset) {
Expand Down Expand Up @@ -194,6 +199,11 @@ export class SLDPManager {
this._transport.runCallback("videoSetup", vsetup);
this._transport.runCallback("audioSetup", asetup);
this._transport.send("timescale", timescale);

this._eventBus.emit(
"nimio:connection-established",
this._context.getStreamsConfig(),
);
}

_pushCurStream(type, stream) {
Expand Down
Loading