web: reconfigure webcodec if needed

This commit is contained in:
Maxim Devaev 2025-03-11 16:07:32 +02:00
parent 2bdd349fbf
commit 8391b7a467
2 changed files with 89 additions and 53 deletions

View File

@ -52,6 +52,9 @@ class _Source:
clients: dict[WsSession, "_Client"] = dataclasses.field(default_factory=dict) clients: dict[WsSession, "_Client"] = dataclasses.field(default_factory=dict)
key_required: bool = dataclasses.field(default=False) key_required: bool = dataclasses.field(default=False)
def is_diff(self) -> bool:
return StreamerFormats.is_diff(self.streamer.get_format())
@dataclasses.dataclass @dataclasses.dataclass
class _Client: class _Client:
@ -98,6 +101,14 @@ class MediaServer(HttpServer):
async def __ws_bin_ping_handler(self, ws: WsSession, _: bytes) -> None: async def __ws_bin_ping_handler(self, ws: WsSession, _: bytes) -> None:
await ws.send_bin(255, b"") # Ping-pong await ws.send_bin(255, b"") # Ping-pong
@exposed_ws(1)
async def __ws_bin_key_handler(self, ws: WsSession, _: bytes) -> None:
for src in self.__srcs:
if ws in src.clients:
if src.is_diff():
src.key_required = True
break
@exposed_ws("start") @exposed_ws("start")
async def __ws_start_handler(self, ws: WsSession, event: dict) -> None: async def __ws_start_handler(self, ws: WsSession, event: dict) -> None:
try: try:
@ -145,7 +156,7 @@ class MediaServer(HttpServer):
# ===== # =====
async def __sender(self, client: _Client) -> None: async def __sender(self, client: _Client) -> None:
need_key = StreamerFormats.is_diff(client.src.streamer.get_format()) need_key = client.src.is_diff()
if need_key: if need_key:
client.src.key_required = True client.src.key_required = True
has_key = False has_key = False

View File

@ -37,6 +37,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
var __ws = null; var __ws = null;
var __ping_timer = null; var __ping_timer = null;
var __missed_heartbeats = 0; var __missed_heartbeats = 0;
var __decoder = null; var __decoder = null;
var __codec = ""; var __codec = "";
var __canvas = $("stream-canvas"); var __canvas = $("stream-canvas");
@ -86,11 +87,15 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
__ws.onerror = __wsErrorHandler; __ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler; __ws.onclose = __wsCloseHandler;
__ws.onmessage = async (event) => { __ws.onmessage = async (event) => {
if (typeof event.data === "string") { try {
event = JSON.parse(event.data); if (typeof event.data === "string") {
__wsJsonHandler(event.event_type, event.event); event = JSON.parse(event.data);
} else { // Binary __wsJsonHandler(event.event_type, event.event);
await __wsBinHandler(event.data); } else { // Binary
await __wsBinHandler(event.data);
}
} catch (ex) {
__wsErrorHandler(ex);
} }
}; };
} }
@ -142,10 +147,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
clearInterval(__ping_timer); clearInterval(__ping_timer);
__ping_timer = null; __ping_timer = null;
} }
if (__decoder) { __closeDecoder();
__decoder.close();
__decoder = null;
}
__missed_heartbeats = 0; __missed_heartbeats = 0;
__fps_accum = 0; __fps_accum = 0;
__ws = null; __ws = null;
@ -156,38 +158,12 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
var __wsJsonHandler = function(event_type, event) { var __wsJsonHandler = function(event_type, event) {
if (event_type === "media") { if (event_type === "media") {
__decoderCreate(event.video); __setupCodec(event.video);
} }
}; };
var __wsBinHandler = async (data) => { var __setupCodec = function(formats) {
let header = new Uint8Array(data.slice(0, 2)); __closeDecoder();
if (header[0] === 255) { // Pong
__missed_heartbeats = 0;
} else if (header[0] === 1 && __decoder !== null) { // Video frame
let key = !!header[1];
if (__decoder.state !== "configured") {
if (!key) {
return;
}
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
__setActive();
}
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
"type": (key ? "key" : "delta"),
"data": data.slice(2),
});
await __decoder.decode(chunk);
}
};
var __decoderCreate = function(formats) {
__decoderDestroy();
if (formats.h264 === undefined) { if (formats.h264 === undefined) {
let msg = "No H.264 stream available on PiKVM"; let msg = "No H.264 stream available on PiKVM";
__setInfo(false, false, msg); __setInfo(false, false, msg);
@ -203,19 +179,76 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
__logInfo(msg); __logInfo(msg);
return; return;
} }
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
"output": __drawFrame,
"error": (err) => __logInfo(err.message),
});
__codec = `avc1.${formats.h264.profile_level_id}`; __codec = `avc1.${formats.h264.profile_level_id}`;
__ws.send(JSON.stringify({ __ws.send(JSON.stringify({
"event_type": "start", "event_type": "start",
"event": {"type": "video", "format": "h264"}, "event": {"type": "video", "format": "h264"},
})); }));
}; };
var __wsBinHandler = async (data) => {
let header = new Uint8Array(data.slice(0, 2));
if (header[0] === 255) { // Pong
__missed_heartbeats = 0;
} else if (header[0] === 1) { // Video frame
let key = !!header[1];
if (await __ensureDecoder(key)) {
await __processFrame(key, data.slice(2));
}
}
};
var __ensureDecoder = async (key) => {
if (__codec === "") {
return false;
}
if (__decoder === null || __decoder.state === "closed") {
let started = (__codec !== "");
let codec = __codec;
__closeDecoder();
__codec = codec;
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
"output": __drawFrame,
"error": (err) => __logInfo(err.message),
});
if (started) {
__ws.send(new Uint8Array([0]));
}
}
if (__decoder.state !== "configured") {
if (!key) {
return false;
}
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
}
if (__decoder.state === "configured") {
__setActive();
return true;
}
return false;
};
var __processFrame = async (key, raw) => {
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
"type": (key ? "key" : "delta"),
"data": raw,
});
await __decoder.decode(chunk);
};
var __closeDecoder = function() {
if (__decoder !== null) {
try {
__decoder.close();
} catch { // eslint-disable-line no-empty
} finally {
__decoder = null;
__codec = "";
}
}
};
var __drawFrame = function(frame) { var __drawFrame = function(frame) {
try { try {
let width = frame.displayWidth; let width = frame.displayWidth;
@ -254,14 +287,6 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) {
} }
}; };
var __decoderDestroy = function() {
if (__decoder !== null) {
__decoder.close();
__decoder = null;
__codec = "";
}
};
var __logInfo = (...args) => tools.info("Stream [Media]:", ...args); var __logInfo = (...args) => tools.info("Stream [Media]:", ...args);
} }