Skip to content

Commit

Permalink
Merge pull request #2 from novnc/master
Browse files Browse the repository at this point in the history
sync
  • Loading branch information
XiaoXianNv-boot committed Sep 5, 2024
2 parents d061b8b + 047531e commit 77ff1d9
Show file tree
Hide file tree
Showing 13 changed files with 828 additions and 32 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ profits such as:
RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman,
UltraVNC's MSLogonII
* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG,
ZRLE, JPEG
ZRLE, JPEG, Zlib
* Supports scaling, clipping and resizing the desktop
* Local cursor rendering
* Clipboard copy/paste with full Unicode support
Expand Down
2 changes: 2 additions & 0 deletions app/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ const UI = {
UI.updateLogging();

/* Populate the controls if defaults are provided in the URL */
UI.initSetting('host', '');
UI.initSetting('port', 0);
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
Expand Down
321 changes: 321 additions & 0 deletions core/decoders/h264.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2024 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/

import * as Log from '../util/logging.js';

export class H264Parser {
constructor(data) {
this._data = data;
this._index = 0;
this.profileIdc = null;
this.constraintSet = null;
this.levelIdc = null;
}

_getStartSequenceLen(index) {
let data = this._data;
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {
return 4;
}
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
return 3;
}
return 0;
}

_indexOfNextNalUnit(index) {
let data = this._data;
for (let i = index; i < data.length; ++i) {
if (this._getStartSequenceLen(i) != 0) {
return i;
}
}
return -1;
}

_parseSps(index) {
this.profileIdc = this._data[index];
this.constraintSet = this._data[index + 1];
this.levelIdc = this._data[index + 2];
}

_parseNalUnit(index) {
const firstByte = this._data[index];
if (firstByte & 0x80) {
throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');
}
const unitType = firstByte & 0x1f;

switch (unitType) {
case 1: // coded slice, non-idr
return { slice: true };
case 5: // coded slice, idr
return { slice: true, key: true };
case 6: // sei
return {};
case 7: // sps
this._parseSps(index + 1);
return {};
case 8: // pps
return {};
default:
Log.Warn("Unhandled unit type: ", unitType);
break;
}
return {};
}

parse() {
const startIndex = this._index;
let isKey = false;

while (this._index < this._data.length) {
const startSequenceLen = this._getStartSequenceLen(this._index);
if (startSequenceLen == 0) {
throw new Error('Invalid start sequence in bit stream');
}

const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);

let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);
if (nextIndex == -1) {
this._index = this._data.length;
} else {
this._index = nextIndex;
}

if (key) {
isKey = true;
}
if (slice) {
break;
}
}

if (startIndex === this._index) {
return null;
}

return {
frame: this._data.subarray(startIndex, this._index),
key: isKey,
};
}
}

export class H264Context {
constructor(width, height) {
this.lastUsed = 0;
this._width = width;
this._height = height;
this._profileIdc = null;
this._constraintSet = null;
this._levelIdc = null;
this._decoder = null;
this._pendingFrames = [];
}

_handleFrame(frame) {
let pending = this._pendingFrames.shift();
if (pending === undefined) {
throw new Error("Pending frame queue empty when receiving frame from decoder");
}

if (pending.timestamp != frame.timestamp) {
throw new Error("Video frame timestamp mismatch. Expected " +
frame.timestamp + " but but got " + pending.timestamp);
}

pending.frame = frame;
pending.ready = true;
pending.resolve();

if (!pending.keep) {
frame.close();
}
}

_handleError(e) {
throw new Error("Failed to decode frame: " + e.message);
}

_configureDecoder(profileIdc, constraintSet, levelIdc) {
if (this._decoder === null || this._decoder.state === 'closed') {
this._decoder = new VideoDecoder({
output: frame => this._handleFrame(frame),
error: e => this._handleError(e),
});
}
const codec = 'avc1.' +
profileIdc.toString(16).padStart(2, '0') +
constraintSet.toString(16).padStart(2, '0') +
levelIdc.toString(16).padStart(2, '0');
this._decoder.configure({
codec: codec,
codedWidth: this._width,
codedHeight: this._height,
optimizeForLatency: true,
});
}

_preparePendingFrame(timestamp) {
let pending = {
timestamp: timestamp,
promise: null,
resolve: null,
frame: null,
ready: false,
keep: false,
};
pending.promise = new Promise((resolve) => {
pending.resolve = resolve;
});
this._pendingFrames.push(pending);

return pending;
}

decode(payload) {
let parser = new H264Parser(payload);
let result = null;

// Ideally, this timestamp should come from the server, but we'll just
// approximate it instead.
let timestamp = Math.round(window.performance.now() * 1e3);

while (true) {
let encodedFrame = parser.parse();
if (encodedFrame === null) {
break;
}

if (parser.profileIdc !== null) {
self._profileIdc = parser.profileIdc;
self._constraintSet = parser.constraintSet;
self._levelIdc = parser.levelIdc;
}

if (this._decoder === null || this._decoder.state !== 'configured') {
if (!encodedFrame.key) {
Log.Warn("Missing key frame. Can't decode until one arrives");
continue;
}
if (self._profileIdc === null) {
Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');
continue;
}
this._configureDecoder(self._profileIdc, self._constraintSet,
self._levelIdc);
}

result = this._preparePendingFrame(timestamp);

const chunk = new EncodedVideoChunk({
timestamp: timestamp,
type: encodedFrame.key ? 'key' : 'delta',
data: encodedFrame.frame,
});

try {
this._decoder.decode(chunk);
} catch (e) {
Log.Warn("Failed to decode:", e);
}
}

// We only keep last frame of each payload
if (result !== null) {
result.keep = true;
}

return result;
}
}

export default class H264Decoder {
constructor() {
this._tick = 0;
this._contexts = {};
}

_contextId(x, y, width, height) {
return [x, y, width, height].join(',');
}

_findOldestContextId() {
let oldestTick = Number.MAX_VALUE;
let oldestKey = undefined;
for (const [key, value] of Object.entries(this._contexts)) {
if (value.lastUsed < oldestTick) {
oldestTick = value.lastUsed;
oldestKey = key;
}
}
return oldestKey;
}

_createContext(x, y, width, height) {
const maxContexts = 64;
if (Object.keys(this._contexts).length >= maxContexts) {
let oldestContextId = this._findOldestContextId();
delete this._contexts[oldestContextId];
}
let context = new H264Context(width, height);
this._contexts[this._contextId(x, y, width, height)] = context;
return context;
}

_getContext(x, y, width, height) {
let context = this._contexts[this._contextId(x, y, width, height)];
return context !== undefined ? context : this._createContext(x, y, width, height);
}

_resetContext(x, y, width, height) {
delete this._contexts[this._contextId(x, y, width, height)];
}

_resetAllContexts() {
this._contexts = {};
}

decodeRect(x, y, width, height, sock, display, depth) {
const resetContextFlag = 1;
const resetAllContextsFlag = 2;

if (sock.rQwait("h264 header", 8)) {
return false;
}

const length = sock.rQshift32();
const flags = sock.rQshift32();

if (sock.rQwait("h264 payload", length, 8)) {
return false;
}

if (flags & resetAllContextsFlag) {
this._resetAllContexts();
} else if (flags & resetContextFlag) {
this._resetContext(x, y, width, height);
}

let context = this._getContext(x, y, width, height);
context.lastUsed = this._tick++;

if (length !== 0) {
let payload = sock.rQshiftBytes(length, false);
let frame = context.decode(payload);
if (frame !== null) {
display.videoFrame(x, y, width, height, frame);
}
}

return true;
}
}
51 changes: 51 additions & 0 deletions core/decoders/zlib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2024 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/

import Inflator from "../inflator.js";

export default class ZlibDecoder {
constructor() {
this._zlib = new Inflator();
this._length = 0;
}

decodeRect(x, y, width, height, sock, display, depth) {
if ((width === 0) || (height === 0)) {
return true;
}

if (this._length === 0) {
if (sock.rQwait("ZLIB", 4)) {
return false;
}

this._length = sock.rQshift32();
}

if (sock.rQwait("ZLIB", this._length)) {
return false;
}

let data = new Uint8Array(sock.rQshiftBytes(this._length, false));
this._length = 0;

this._zlib.setInput(data);
data = this._zlib.inflate(width * height * 4);
this._zlib.setInput(null);

// Max sure the image is fully opaque
for (let i = 0; i < width * height; i++) {
data[i * 4 + 3] = 255;
}

display.blitImage(x, y, width, height, data, 0);

return true;
}
}
Loading

0 comments on commit 77ff1d9

Please sign in to comment.