default jpeg
This commit is contained in:
@@ -1736,11 +1736,7 @@ func desktopCodec() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func backstageCodec() string {
|
func backstageCodec() string {
|
||||||
codec := selectedCodec(&backstageOverrideCodec)
|
return "jpeg"
|
||||||
if codec == "hevc" {
|
|
||||||
return "jpeg"
|
|
||||||
}
|
|
||||||
return codec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDesktopQualityAndCodec(quality int, codec string) {
|
func SetDesktopQualityAndCodec(quality int, codec string) {
|
||||||
@@ -1792,36 +1788,15 @@ func SetDesktopQualityAndCodec(quality int, codec string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetBackstageQualityAndCodec(quality int, codec string) {
|
func SetBackstageQualityAndCodec(quality int, _ string) {
|
||||||
previousCodec := backstageCodec()
|
|
||||||
if quality > 0 {
|
if quality > 0 {
|
||||||
if quality > 100 {
|
if quality > 100 {
|
||||||
quality = 100
|
quality = 100
|
||||||
}
|
}
|
||||||
backstageOverrideQuality.Store(int64(quality))
|
backstageOverrideQuality.Store(int64(quality))
|
||||||
}
|
}
|
||||||
s := strings.ToLower(strings.TrimSpace(codec))
|
backstageOverrideCodec.Store("jpeg")
|
||||||
if s == "h264" && !h264Available() {
|
resetH264Encoderbackstage()
|
||||||
detail := h264AvailabilityDetail()
|
|
||||||
log.Printf("capture: requested backstage codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
|
|
||||||
s = "jpeg"
|
|
||||||
}
|
|
||||||
switch s {
|
|
||||||
case "raw", "rgba", "jpeg", "h264":
|
|
||||||
backstageOverrideCodec.Store(s)
|
|
||||||
backstageH264WarnOnce = sync.Once{}
|
|
||||||
if s != "h264" {
|
|
||||||
resetH264Encoderbackstage()
|
|
||||||
}
|
|
||||||
if previousCodec != s && s == "h264" {
|
|
||||||
backstageLastKeyframe.Store(0)
|
|
||||||
webrtcpub.RequestKeyframe(webrtcpub.Kindbackstage)
|
|
||||||
}
|
|
||||||
case "":
|
|
||||||
default:
|
|
||||||
backstageOverrideCodec.Store("jpeg")
|
|
||||||
resetH264Encoderbackstage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Nowbackstage(ctx context.Context, env *rt.Env) error {
|
func Nowbackstage(ctx context.Context, env *rt.Env) error {
|
||||||
|
|||||||
@@ -77,6 +77,20 @@ func TestDesktopAndBackstageCodecSelectionsAreIndependent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackstageCodecIsAlwaysJPEG(t *testing.T) {
|
||||||
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
|
for _, requested := range []string{"", "raw", "rgba", "h264", "hevc", "invalid-codec"} {
|
||||||
|
SetBackstageQualityAndCodec(73, requested)
|
||||||
|
if got := backstageCodec(); got != "jpeg" {
|
||||||
|
t.Fatalf("backstage codec = %q after requesting %q, want jpeg", got, requested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := backstageJPEGQuality(); got != 73 {
|
||||||
|
t.Fatalf("backstage quality = %d, want 73", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSwitchingToH264RequestsRecoveryPoint(t *testing.T) {
|
func TestSwitchingToH264RequestsRecoveryPoint(t *testing.T) {
|
||||||
if !h264Available() {
|
if !h264Available() {
|
||||||
t.Skip("h264 is unavailable in this build")
|
t.Skip("h264 is unavailable in this build")
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
const CANVAS_TRANSPORT_PREF_VERSION = 1;
|
const CANVAS_TRANSPORT_PREF_VERSION = 1;
|
||||||
|
|
||||||
function getWebrtcMode() {
|
function getWebrtcMode() {
|
||||||
return webrtcMode ? String(webrtcMode.value || "off") : "off";
|
return "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncInputEnableState() {
|
function syncInputEnableState() {
|
||||||
@@ -151,7 +151,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
let h264TimestampUs = 0;
|
let h264TimestampUs = 0;
|
||||||
let h264AwaitingKeyframe = true;
|
let h264AwaitingKeyframe = true;
|
||||||
let h264RecoveryRequestedAt = 0;
|
let h264RecoveryRequestedAt = 0;
|
||||||
let prefersH264 = typeof VideoDecoder === "function";
|
let prefersH264 = false;
|
||||||
const maxH264DecodeQueue = 4;
|
const maxH264DecodeQueue = 4;
|
||||||
const inputBackpressureBytes = 256 * 1024;
|
const inputBackpressureBytes = 256 * 1024;
|
||||||
let h264ErrorCount = 0;
|
let h264ErrorCount = 0;
|
||||||
@@ -182,15 +182,13 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
savedDisplay = Number.isFinite(Number(settings.display)) ? Number(settings.display) : savedDisplay;
|
||||||
setSelectValue(backstageResolutionSelect, settings.resolution);
|
setSelectValue(backstageResolutionSelect, settings.resolution);
|
||||||
setSelectValue(targetFpsSelect, settings.targetFps);
|
setSelectValue(targetFpsSelect, settings.targetFps);
|
||||||
setSelectValue(webrtcMode, settings.transportPreferenceVersion === CANVAS_TRANSPORT_PREF_VERSION ? settings.webrtcMode : "off");
|
setSelectValue(webrtcMode, "off");
|
||||||
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
if (qualitySlider && settings.quality !== undefined) qualitySlider.value = String(settings.quality);
|
||||||
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
if (mouseCtrl && typeof settings.mouse === "boolean") mouseCtrl.checked = settings.mouse;
|
||||||
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
|
||||||
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
|
||||||
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
if (uiaCtrl && typeof settings.uia === "boolean") uiaCtrl.checked = settings.uia;
|
||||||
if (typeof settings.preferH264 === "boolean") {
|
prefersH264 = false;
|
||||||
prefersH264 = settings.preferH264 && typeof VideoDecoder === "function";
|
|
||||||
}
|
|
||||||
const cloneToggle = document.getElementById("backstageCloneToggle");
|
const cloneToggle = document.getElementById("backstageCloneToggle");
|
||||||
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
|
const cloneLiteToggle = document.getElementById("backstageCloneLiteToggle");
|
||||||
const killIfRunningToggle = document.getElementById("backstageKillIfRunningToggle");
|
const killIfRunningToggle = document.getElementById("backstageKillIfRunningToggle");
|
||||||
@@ -208,8 +206,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
resolution: backstageResolutionSelect?.value || "1080",
|
resolution: backstageResolutionSelect?.value || "1080",
|
||||||
targetFps: Number(targetFpsSelect?.value || 120),
|
targetFps: Number(targetFpsSelect?.value || 120),
|
||||||
quality: Number(qualitySlider?.value || 90),
|
quality: Number(qualitySlider?.value || 90),
|
||||||
preferH264: !!prefersH264,
|
preferH264: false,
|
||||||
webrtcMode: getWebrtcMode(),
|
webrtcMode: "off",
|
||||||
transportPreferenceVersion: CANVAS_TRANSPORT_PREF_VERSION,
|
transportPreferenceVersion: CANVAS_TRANSPORT_PREF_VERSION,
|
||||||
mouse: !!mouseCtrl?.checked,
|
mouse: !!mouseCtrl?.checked,
|
||||||
keyboard: !!kbdCtrl?.checked,
|
keyboard: !!kbdCtrl?.checked,
|
||||||
@@ -225,8 +223,12 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
const sharedSettingsSaver = createSharedUiSettingsSaver("backstage", readSharedSettings);
|
const sharedSettingsSaver = createSharedUiSettingsSaver("backstage", readSharedSettings);
|
||||||
|
|
||||||
if (codecH264) {
|
if (codecH264) {
|
||||||
codecH264.checked = prefersH264;
|
codecH264.checked = false;
|
||||||
codecH264.disabled = typeof VideoDecoder !== "function";
|
codecH264.disabled = true;
|
||||||
|
}
|
||||||
|
if (webrtcMode) {
|
||||||
|
webrtcMode.value = "off";
|
||||||
|
webrtcMode.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCodecModeLabel(mode, detail) {
|
function setCodecModeLabel(mode, detail) {
|
||||||
@@ -235,7 +237,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
codecMode.textContent = `Codec: ${String(mode || "auto").toUpperCase()}${suffix}`;
|
codecMode.textContent = `Codec: ${String(mode || "auto").toUpperCase()}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCodecModeLabel(prefersH264 ? "h264" : "jpeg", "preferred");
|
setCodecModeLabel("jpeg", "enforced");
|
||||||
setStreamState("connecting", "Connecting");
|
setStreamState("connecting", "Connecting");
|
||||||
|
|
||||||
function updateFpsDisplay(agentValue) {
|
function updateFpsDisplay(agentValue) {
|
||||||
@@ -1061,7 +1063,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
|
|
||||||
function pushQuality(val) {
|
function pushQuality(val) {
|
||||||
const q = Number(val) || 90;
|
const q = Number(val) || 90;
|
||||||
const codec = q >= 100 ? "raw" : (prefersH264 ? "h264" : "jpeg");
|
const codec = "jpeg";
|
||||||
console.debug("backstage: pushQuality val=", val, "q=", q, "codec=", codec);
|
console.debug("backstage: pushQuality val=", val, "q=", q, "codec=", codec);
|
||||||
setCodecModeLabel(codec, "requested");
|
setCodecModeLabel(codec, "requested");
|
||||||
sendCmd("backstage_set_quality", { quality: q, codec });
|
sendCmd("backstage_set_quality", { quality: q, codec });
|
||||||
@@ -1084,12 +1086,6 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pushTransportQuality(mode) {
|
function pushTransportQuality(mode) {
|
||||||
if (mode === "relayed" || mode === "p2p") {
|
|
||||||
const q = Number(qualitySlider?.value) || 90;
|
|
||||||
setCodecModeLabel("h264", "webrtc");
|
|
||||||
sendCmd("backstage_set_quality", { quality: q, codec: "h264", source: "webrtc" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (qualitySlider) {
|
if (qualitySlider) {
|
||||||
pushQuality(qualitySlider.value);
|
pushQuality(qualitySlider.value);
|
||||||
}
|
}
|
||||||
@@ -1097,10 +1093,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
|
|
||||||
if (codecH264) {
|
if (codecH264) {
|
||||||
codecH264.addEventListener("change", function () {
|
codecH264.addEventListener("change", function () {
|
||||||
prefersH264 = !!codecH264.checked && typeof VideoDecoder === "function";
|
prefersH264 = false;
|
||||||
if (!prefersH264) {
|
codecH264.checked = false;
|
||||||
destroyVideoDecoder();
|
destroyVideoDecoder();
|
||||||
}
|
|
||||||
if (qualitySlider) {
|
if (qualitySlider) {
|
||||||
pushQuality(qualitySlider.value);
|
pushQuality(qualitySlider.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,10 +152,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<label class="text-sm text-slate-300 w-20" for="webrtcMode">Transport</label>
|
<label class="text-sm text-slate-300 w-20" for="webrtcMode">Transport</label>
|
||||||
<select id="webrtcMode" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100" title="Streaming transport. P2P: browser to agent direct, server only relays signaling. Relayed: through MediaMTX.">
|
<select id="webrtcMode" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-100" title="Backstage uses JPEG over canvas." disabled>
|
||||||
<option value="off" selected>Canvas</option>
|
<option value="off" selected>JPEG Canvas</option>
|
||||||
<option value="p2p">WebRTC P2P</option>
|
|
||||||
<option value="relayed">WebRTC Relayed</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
@@ -171,10 +169,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Prefer H.264 when available; disable to force JPEG">
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Backstage is restricted to JPEG">
|
||||||
<input type="checkbox" id="codecH264" checked /> H.264
|
<input type="checkbox" id="codecH264" disabled /> H.264 disabled
|
||||||
</label>
|
</label>
|
||||||
<span id="codecMode" class="pill pill-ghost text-slate-100 text-xs">Codec: auto</span>
|
<span id="codecMode" class="pill pill-ghost text-slate-100 text-xs">Codec: JPEG</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -911,7 +911,7 @@ type backstageStreamingState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function defaultbackstageStreamingState(): backstageStreamingState {
|
function defaultbackstageStreamingState(): backstageStreamingState {
|
||||||
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
|
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "jpeg", maxFps: 120, lastFps: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backstageStreamingState = new Map<string, backstageStreamingState>();
|
export const backstageStreamingState = new Map<string, backstageStreamingState>();
|
||||||
@@ -1279,26 +1279,9 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
|
|||||||
sendbackstageCommand(target, "backstage_stop", {});
|
sendbackstageCommand(target, "backstage_stop", {});
|
||||||
state.isStreaming = false;
|
state.isStreaming = false;
|
||||||
logger.debug(`[backstage] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
logger.debug(`[backstage] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
|
||||||
}
|
}
|
||||||
if (!state.isStreaming) {
|
if (!state.isStreaming) {
|
||||||
if ((payload as any).webrtc === true) {
|
sendbackstageCommand(target, "backstage_set_quality", { quality: state.quality, codec: "jpeg" });
|
||||||
const streamPath = webrtcStreamPathFor(clientId, "backstage");
|
|
||||||
const token = issueWebrtcPublishToken(clientId);
|
|
||||||
sendbackstageCommand(target, "webrtc_publish", {
|
|
||||||
streamPath,
|
|
||||||
whipPath: `/api/webrtc/${streamPath}/whip`,
|
|
||||||
token,
|
|
||||||
kind: "backstage",
|
|
||||||
hasVideo: true,
|
|
||||||
hasAudio: false,
|
|
||||||
iceServers: issueTurnIceServers(`${clientId}:backstage:whip`),
|
|
||||||
});
|
|
||||||
safeSendViewer(ws, {
|
|
||||||
type: "webrtc_ready",
|
|
||||||
streamPath,
|
|
||||||
whepPath: `/api/webrtc/${streamPath}/whep`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
sendbackstageCommand(target, "backstage_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
sendbackstageCommand(target, "backstage_set_fps", { fps: clampDesktopFps(state.maxFps) });
|
||||||
sendbackstageCommand(target, "backstage_start", {
|
sendbackstageCommand(target, "backstage_start", {
|
||||||
autoStartExplorer: false,
|
autoStartExplorer: false,
|
||||||
@@ -1341,9 +1324,9 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
|
|||||||
}
|
}
|
||||||
case "backstage_set_quality": {
|
case "backstage_set_quality": {
|
||||||
const newQuality = Number(payload.quality) || 90;
|
const newQuality = Number(payload.quality) || 90;
|
||||||
const newCodec = String(payload.codec || "").toLowerCase();
|
const newCodec = "jpeg";
|
||||||
sendbackstageCommand(target, "backstage_set_quality", { quality: newQuality, codec: newCodec });
|
sendbackstageCommand(target, "backstage_set_quality", { quality: newQuality, codec: newCodec });
|
||||||
if (state.quality !== newQuality || state.codec !== newCodec) {
|
if (state.quality !== newQuality || state.codec !== newCodec) {
|
||||||
state.quality = newQuality;
|
state.quality = newQuality;
|
||||||
state.codec = newCodec;
|
state.codec = newCodec;
|
||||||
backstageStreamingState.set(clientId, state);
|
backstageStreamingState.set(clientId, state);
|
||||||
@@ -1512,24 +1495,10 @@ export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, ra
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "webrtc_p2p_offer": {
|
case "webrtc_p2p_offer": {
|
||||||
const sdp = typeof (payload as any).sdp === "string" ? (payload as any).sdp : "";
|
safeSendViewer(ws, { type: "error", reason: "Backstage supports JPEG canvas transport only" });
|
||||||
if (!sdp) break;
|
|
||||||
const sessionId = createP2PSession(ws, clientId, "backstage");
|
|
||||||
sendbackstageCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "backstage", hasVideo: true, hasAudio: false, iceServers: issueTurnIceServers(`${clientId}:backstage:${sessionId}`) });
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "webrtc_p2p_ice": {
|
case "webrtc_p2p_ice": {
|
||||||
const sessionId = getP2PSessionIdForViewer(ws);
|
|
||||||
if (!sessionId) break;
|
|
||||||
const candidate = typeof (payload as any).candidate === "string" ? (payload as any).candidate : "";
|
|
||||||
if (!candidate) break;
|
|
||||||
sendbackstageCommand(target, "webrtc_p2p_ice", {
|
|
||||||
sessionId,
|
|
||||||
kind: "backstage",
|
|
||||||
candidate,
|
|
||||||
sdpMid: typeof (payload as any).sdpMid === "string" ? (payload as any).sdpMid : "",
|
|
||||||
sdpMLineIndex: Number((payload as any).sdpMLineIndex) || 0,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "webrtc_p2p_stop": {
|
case "webrtc_p2p_stop": {
|
||||||
|
|||||||
Reference in New Issue
Block a user