diff --git a/js/audio.js b/js/audio.js index b8228b7..ef5dd8f 100644 --- a/js/audio.js +++ b/js/audio.js @@ -5,6 +5,10 @@ import { wait, on, off } from './util.js'; let ctx; let enabled = true; +let source; +export const getAudioCtx = () => ctx; +/** @returns {MediaStreamAudioSourceNode} */ +export const getAudioSource = () => source; export async function start(attempts = 1) { if (ctx || !enabled) @@ -38,7 +42,7 @@ export async function start(attempts = 1) { noiseSuppression: false } }) - const source = ctx.createMediaStreamSource(stream); + source = ctx.createMediaStreamSource(stream); source.connect(ctx.destination); if (ctx.state !== 'running') { diff --git a/js/gl-renderer.js b/js/gl-renderer.js index a8999fc..df6426e 100644 --- a/js/gl-renderer.js +++ b/js/gl-renderer.js @@ -36,6 +36,7 @@ export class Renderer { this._canvas = document.getElementById('canvas') this._gl = this._canvas.getContext('webgl2', { + preserveDrawingBuffer: true, alpha: false, antialias: false }); diff --git a/js/main.js b/js/main.js index f8a667e..6e60c83 100644 --- a/js/main.js +++ b/js/main.js @@ -4,6 +4,7 @@ import { UsbConnection } from './usb.js'; import { SerialConnection } from './serial.js'; import { Parser } from './parser.js'; +import { Recorder } from "./recorder.js" import { Renderer as OldRenderer } from './renderer.js'; import { Renderer as GlRenderer } from './gl-renderer.js'; import { show, hide, toggle, appendButton, on } from './util.js'; @@ -28,6 +29,7 @@ const renderer = Settings.get('displayType') === 'webgl2' : new OldRenderer(bg, setBackground); const parser = new Parser(renderer); +const recorder = new Recorder(); let resizeCanvas = (function() { const display = document.getElementById('display'); @@ -118,6 +120,16 @@ Settings.onChange('fullscreen', () => { Settings.onChange('about', () => show('#info')); +Settings.onChange('record', (value) => { + if (value) { + recorder.startRecording(); + } else { + recorder.stopRecording(); + } +}) + +Settings.onChange('screenshot', () => recorder.screenshot(renderer._ctx)) + function connectionChanged(isConnected) { if (isConnected) { hide('#buttons, .error, #info'); diff --git a/js/recorder.js b/js/recorder.js new file mode 100644 index 0000000..d84389e --- /dev/null +++ b/js/recorder.js @@ -0,0 +1,84 @@ + +import { getAudioSource } from "./audio.js" + +const downloadFile = (name, data) => { + const link = document.createElement('a'); + link.download = name; + link.href = data; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +const filename = (postfix, fileType) => { + const [date, time] = new Date().toISOString().replace('Z', '').split('T') + const looseTime = time.split('.')[0].replaceAll(':', '.') + return `${date} ${looseTime} ${postfix}.${fileType}` +} + +export class Recorder { + /** @type {HTMLCanvasElement} */ + _canvas; + /** @type {CanvasRenderingContext2D} */ + _ctx; + + /** @type {MediaRecorder | undefined} */ + _recorder; + /** @type {MediaStream | undefined} */ + _stream; + + + constructor() { + this._canvas = document.getElementById('canvas') + this._ctx = this._canvas.getContext('2d'); + } + + screenshot(ctx) { + this._canvas.toBlob((blob) => { + if (blob === null) { + return; + } + const url = URL.createObjectURL(blob) + downloadFile(filename('m8-screenshot', 'png'), url) + }) + } + + startRecording() { + if (this._recorder !== undefined && this._recorder.state === 'recording') { + return; + } + this._stream = this._canvas.captureStream(); + for (const track of getAudioSource().mediaStream.getAudioTracks()) { + this._stream.addTrack(track); + } + + this._recorder = new MediaRecorder(this._stream, { + mimeType: "video/webm" + }); + const chunks = []; + this._recorder.addEventListener('dataavailable', (event) => { + chunks.push(event.data); + }) + this._recorder.addEventListener('stop', () => { + const url = URL.createObjectURL(new Blob(chunks)); + downloadFile(filename('m8-recording', 'webm'), url) + chunks.length = 0; + }) + this._recorder.start(); + } + + stopRecording() { + if (this._recorder !== undefined) { + if (this._recorder.state === 'recording') { + this._recorder.stop() + } + this._recorder = undefined + } + if (this._stream !== undefined) { + for (const track of this._stream.getVideoTracks()) { + track.stop(); + } + this._stream = undefined; + } + } +} \ No newline at end of file diff --git a/js/settings.js b/js/settings.js index 0929f72..df5122d 100644 --- a/js/settings.js +++ b/js/settings.js @@ -24,9 +24,11 @@ setupSelect( { webgl2: 'WebGL2', old: 'Canvas + SVG' }, 'webgl2'); +setupToggle('record', 'Record Screen', false); setupToggle('snapPixels', 'Snap Pixels', true); setupToggle('virtualKeyboard', 'Virtual Keyboard', true); setupToggle('preventSleep', 'Prevent Sleep', false); +setupButton('screenshot', 'Take Screenshot'); setupButton('controlMapping', 'Control Mapping'); setupButton('firmware', 'Load Firmware'); setupButton('fullscreen', 'Fullscreen');