Skip to content

Commit

Permalink
Merge pull request #6 from h-banii/feat/high-quality-video
Browse files Browse the repository at this point in the history
feat: Record high quality videos
  • Loading branch information
h-banii authored Feb 15, 2024
2 parents 1219da3 + 27e3b48 commit 6a520f7
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 42 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ npm start
- height: height in pixels of the canvas
- hide_buttons: hides buttons (useful if you want to record it on OBS, for example)
- fps: changes fps used to record the canvas
- mbps: changes bitrate (mbps) used to record the canvas
- mime: changes mime type used to record the canvas

Here's an example using all of them:

Expand Down Expand Up @@ -127,5 +129,4 @@ simple web page that works inside a regular browser...
- [X] Record videos in the browser
- [X] Show buttons on the screen
- [X] Deploy to GitHub Pages
- [ ] Record videos in high quality
- [ ] Record gifs in the browser
22 changes: 16 additions & 6 deletions src/file/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,23 @@ class Timestamp {
}

export class CanvasRecorder extends EventTarget {
constructor(canvas, fps) {
constructor(canvas, fps, mbps, mime) {
super();

this.canvas = canvas;
const mimeIsSupported = MediaRecorder.isTypeSupported(mime);

if (!mimeIsSupported)
console.log(`Unsupported mime type: ${mime}`)

this.fps = fps;
this.mbps = mbps;

this.chunks = [];
this.stream = canvas.captureStream(fps);
this.recorder = new MediaRecorder(this.stream);
this.recorder = new MediaRecorder(this.stream, {
videoBitsPerSecond: mbps * 1e6,
mimeType: mimeIsSupported ? mime : '',
});
this.recording = false;

this.timestamp = new Timestamp((time) => {
Expand Down Expand Up @@ -143,11 +151,13 @@ export class CanvasRecorder extends EventTarget {
this.dispatchResetEvent();
}

save(filename = 'hypr-shader-preview-video') {
const blob = new Blob(this.chunks, { 'type' : 'video/webm' });
save(filename = 'hypr-shader-preview-video', type = 'video/mp4') {
console.log(
`[${new Date().toLocaleString()}] Downloading recording: ${this.fps} fps; ${this.mbps} mbps; ${type}`
)
const blob = new Blob(this.chunks, { 'type' : type });
const url = URL.createObjectURL(blob);
download(filename, url);
this.reset();
}

dispatchRecordingEvent(recording) {
Expand Down
88 changes: 59 additions & 29 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { loadShader, loadTexture, createShader, createProgram, createContext } from './webgl';
import { Animation } from './animation';
import { askForFile, screenshotCanvas, CanvasRecorder, readFileAsText, readFileAsDataURL } from './file';
import { doubleClick, queryParameters, generateFilename, createElement } from './utils';
import { doubleClick, queryParameters, generateFilename, createElement, createInput } from './utils';

import vertexSrc from '/shaders/default.vert?url&raw';
import vertex3Src from '/shaders/default3.vert?url&raw';

async function main({ shader, image, width, height, fps, hide_buttons }) {
async function main({ shader, image, width, height, fps, mbps, mime, hide_buttons }) {
console.log(`
shader: ${shader}
image: ${image}
width: ${width}
height: ${height}
fps: ${fps}
mbps: ${mbps}
mime: ${mime}
hide_buttons: ${hide_buttons}
`)

const gl = createContext(width, height);

const fragSrc = await loadShader(`./shaders/${shader}`)
const texture = await loadTexture(gl, `./images/${image}`);

const recorder = new CanvasRecorder(gl.canvas, fps);
const recorder = new CanvasRecorder(gl.canvas, fps, mbps, mime);
const animation = new Animation;

try {
Expand All @@ -26,11 +37,12 @@ async function main({ shader, image, width, height, fps, hide_buttons }) {
const filename = () =>
generateFilename('hypr-shader-preview', shader, image);

if (hide_buttons)
if (hide_buttons) {
configureClickActions(gl, texture, animation, filename);
else
configureKeyboardActions(recorder, filename);
} else {
configureButtonActions(gl, fragSrc, texture, animation, recorder, filename);
configureKeyboardActions(recorder, filename);
}
}

function configureButtonActions(gl, fragSrc, texture, animation, recorder, filename) {
Expand Down Expand Up @@ -95,6 +107,13 @@ function configureButtonActions(gl, fragSrc, texture, animation, recorder, filen
}),
]});

const mimeTypeInput = createInput({
type: 'text',
classList: 'button',
size: "12",
value: 'video/mp4',
});

const recordingButtons = createElement({ classList: 'bottom left', children: [
createElement({
classList: 'button',
Expand All @@ -116,45 +135,52 @@ function configureButtonActions(gl, fragSrc, texture, animation, recorder, filen
createElement({
type: 'button',
innerText: '◎ record',
onclick: function() {
if (recorder.recording) {
recorder.stop();
} else {
recorder.start();
}
},
setup: self => {
self.onclick = function() {
if (recorder.recording) {
recorder.stop();
self.style.display = 'none';
} else {
recorder.start();
}
};
recorder.addEventListener('recording', e => {
self.innerText = e.detail ? '◉ stop' : '◎ record';
})
recorder.addEventListener('reset', e => {
self.style.display = '';
})
},
}),
createElement({
type: 'button',
innerText: 'save',
style: 'display: none',
onclick: function() {
recorder.save(filename());
},
children: [
mimeTypeInput,
createElement({
type: 'button',
innerText: 'save',
onclick: function() {
recorder.save(filename(), mimeTypeInput.value);
},
}),
createElement({
type: 'button',
innerText: 'cancel',
onclick: function() {
recorder.reset();
},
}),
],
setup: self => {
recorder.addEventListener('recording', e => {
const recording = e.detail;
if (!recording) {
self.style.display = '';
} else {
self.style.display = 'none';
}
if (!e.detail) self.style.display = 'inline-block';
else self.style.display = 'none';
})
recorder.addEventListener('reset', () => {
self.style.display = 'none';
})
},
}),
createElement({
classList: 'button',
style: 'display: inline-block',
innerText: '(this is low quality, see README for alternative)'
})
]});

document.body.append(creditButtons);
Expand Down Expand Up @@ -259,11 +285,15 @@ function initTextureSampler(gl, program, texture, unit=0) {
gl.uniform1i(gl.getUniformLocation(program, "tex"), unit);
}

const isFirefox = typeof InstallTrigger !== 'undefined';

queryParameters(main, {
"shader" : 'default.frag',
"image" : 'default.png',
"width" : window.innerWidth,
"height" : window.innerHeight,
"fps" : 30,
"mbps": 26,
"mime": `video/webm; codecs="${isFirefox ? 'vp8' : 'vp9'}"`,
"hide_buttons": false,
})
14 changes: 8 additions & 6 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ export const generateFilename = function(prefix, ...parameters) {
return filename;
}

export function createButton(container) {
const element = document.createElement('button');
if (container) container.appendChild(element);
return element;
}

export function createElement({ type = 'div', children = [], setup = () => {}, ...rest }) {
const element = document.createElement(type);

Expand All @@ -65,3 +59,11 @@ export function createElement({ type = 'div', children = [], setup = () => {}, .

return element;
}

export function createInput({ type = 'text', ...rest }) {
const element = createElement({ type: 'input', ...rest });

element.type = type;

return element;
}

0 comments on commit 6a520f7

Please sign in to comment.