Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] QEMU Extended Audio Support #1456

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 84 additions & 0 deletions core/rfb.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold } from './util/browser.js';
import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js';
import AudioBuffer from './util/audio.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import Inflator from "./inflator.js";
Expand Down Expand Up @@ -499,6 +500,15 @@ export default class RFB extends EventTargetMixin {
}
}

enableAudio(sampleFormat, channels, frequency) {
RFB.messages.SetQEMUExtendedAudioFormat(this._sock, sampleFormat, channels, frequency);
RFB.messages.ToggleQEMUExtendedAudio(this._sock, true);
}

disableAudio() {
RFB.messages.ToggleQEMUExtendedAudio(this._sock, false);
}

// ===== PRIVATE METHODS =====

_connect() {
Expand Down Expand Up @@ -2039,6 +2049,25 @@ export default class RFB extends EventTargetMixin {
return true;
}

_handleQEMUExtAudioMsg() {
if (this._sock.rQwait("QEMU extended audio message", 3, 1)) { return false; }

this._sock.rQshift8(); // for now there is only a single submessage type 1
const operation = this._sock.rQshift16();

if (operation === 1) { // stream is starting
this._audioBuffer = new AudioBuffer('audio/webm; codecs="opus"'); // TODO: This is obviously not the right value to use here
} else if (operation === 0) { // stream is stopping
this._audioBuffer.close();
} else { // stream data
const length = this._sock.rQshift32();
const data = this._sock.rQshiftBytes(length);
this._audioBuffer.queueAudio(data);
}

return true;
}

_handleXvpMsg() {
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
this._sock.rQskipBytes(1); // Padding
Expand Down Expand Up @@ -2113,6 +2142,9 @@ export default class RFB extends EventTargetMixin {
case 250: // XVP
return this._handleXvpMsg();

case 255: // Qemu extended audio message
return this._handleQEMUExtAudioMsg();

default:
this._fail("Unexpected server message (type " + msgType + ")");
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
Expand Down Expand Up @@ -2549,6 +2581,16 @@ export default class RFB extends EventTargetMixin {
}
}

// Audio sample formats
RFB.sampleFormats = {
U8: 0,
S8: 1,
U16: 2,
S16: 3,
U32: 4,
S32: 5
};

// Class Methods
RFB.messages = {
keyEvent(sock, keysym, down) {
Expand All @@ -2570,6 +2612,48 @@ RFB.messages = {
sock.flush();
},

ToggleQEMUExtendedAudio(sock, enabled) {
const buff = sock._sQ;
const offset = sock._sQlen;

buff[offset] = 255; // msg-type
buff[offset + 1] = 1; // sub msg-type

buff[offset + 2] = 0; // operation
if (enabled) {
buff[offset + 3] = 0;
} else {
buff[offset + 3] = 1;
}

sock._sQlen += 4;
sock.flush();
},

SetQEMUExtendedAudioFormat(sock, sampleFormat, channels, frequency) {
const buff = sock._sQ;
const offset = sock._sQlen;

buff[offset] = 255; // msg type
buff[offset + 1] = 1; // sub msg-type

buff[offset + 2] = 0; // operation
buff[offset + 3] = 2;

buff[offset + 4] = sampleFormat;
buff[offset + 5] = channels;

const freq = toUnsigned32bit(frequency);

buff[offset + 6] = freq >> 24;
buff[offset + 7] = freq >> 16;
buff[offset + 8] = freq >> 8;
buff[offset + 9] = freq;

sock._sQlen += 10;
sock.flush();
},

QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
function getRFBkeycode(xtScanCode) {
const upperByte = (keycode >> 8);
Expand Down
48 changes: 48 additions & 0 deletions core/util/audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/

export default class AudioBuffer {
constructor(codec) {
this._codec = codec
// instantiate a media source and audio buffer/queue
this._mediaSource = new MediaSource();
this._audioBuffer = null;
this._audioQ = [];

// create a hidden audio element
this._audio = document.createElement('audio');
this._audio.src = window.URL.createObjectURL(this._mediaSource);

// when data is queued, start playing
this._mediaSource.addEventListener('sourceopen', this._onSourceOpen, false);
}

_onSourceOpen(e) {
this._audio.play();
this._audioBuffer = this._mediaSource.addSourceBuffer(this._codec);
this._audioBuffer.addEventListener('update', this._onUpdateBuffer);
}

_onUpdateBuffer() {
if (this._audioQ.length > 0 && !this._audioBuffer.updating) {
this._audioBuffer.appendBuffer(this._audioQ.shift());
}
}

queueAudio(data) {
if (this._audioBuffer !== null) {
if (this._audioBuffer.updating || this._audioQ.length > 0) {
this._audioQ.push(data);
} else {
this._audioBuffer.appendBuffer(data);
}
}
}

close() {} // intentionally left empty as no cleanup seems necessary
}