PSC practice quiz video generator. Paste your questions, get a shareable video.
- Open
index.htmlin a Chromium-based browser (Chrome, Edge, Brave).Firefox is not recommended —
MediaRecorderMP4 support varies. - The textarea is pre-filled with sample questions. Replace the content with your own quiz data (see format below).
- Click Generate Video.
- Keep the tab in the foreground while the video generates.
- When complete, the video downloads automatically.
The textarea expects a JavaScript array assigned to const quizData. Each item in the array is one question.
const quizData = [
{
q: "Your question text here",
options: ["Option A", "Option B", "Option C", "Option D"],
correct: 0, // zero-based index of the correct option
},
{
q: "Second question",
options: ["Apple", "Banana", "Mango", "Grape"],
correct: 2, // "Mango" is correct
},
];| Field | Type | Description |
|---|---|---|
q |
string |
The question text. Supports Malayalam and other Unicode. |
options |
array |
Exactly 4 answer choices. |
correct |
number |
Index (0–3) of the correct answer. |
The app uses
eval()internally to parse the data, so standard JS array syntax is required.
Each question goes through four phases automatically:
| Phase | Duration | What happens |
|---|---|---|
| Typing | ~50 ms per character | Question text types out character by character with keyboard sound |
| Options reveal | 1 second | All four answer boxes appear |
| Countdown | 3 seconds | A 3-2-1 timer ticks down with audio beeps |
| Answer reveal | 2.5 seconds | Correct answer box highlights green |
The total video duration depends on question count and question length.
- Format: MP4 (H.264/AVC) if the browser supports it, otherwise WebM (VP9)
- Resolution: 1080 × 1920 pixels (vertical / portrait — optimised for Reels/Shorts)
- Frame rate: 30 FPS
- Bitrate: 8 Mbps video
- Audio: Mixed stereo from the Web Audio API (keyboard clicks + countdown beeps)
- Filename:
Quiz_HD_<timestamp>.mp4
- Keep the tab focused. The browser releases the screen wake lock when the tab is hidden. The app re-acquires it when you return, but brief tab switches can cause dropped frames.
- Use short questions. Very long question strings will increase per-question duration because the typing phase scales with character count.
- Malayalam font. The app loads Noto Sans Malayalam from Google Fonts. Make sure you have an internet connection when opening the page, otherwise fallback fonts may render incorrectly.
- Batch generation. Paste all your questions in one run. Each generation is one continuous video, so it is more efficient than generating questions individually.
Quiz Maker is a single-file web application (index.html). It has no build step, no server, and no external runtime dependencies beyond CDN-loaded fonts and Tailwind CSS. All video encoding happens entirely in the browser using standard Web APIs.
index.html
├── <head> — Tailwind CSS (CDN), Noto Sans Malayalam (Google Fonts)
├── #ui-setup — Input panel (textarea + generate button)
├── #ui-render — Live preview canvas + status text
├── <style> — Minimal custom CSS (layout, canvas, badge)
└── <script> — All application logic (~300 lines)
quizData array
│
▼
startQuizProcess() ← async loop over each question
│
├── Typing phase → render() called per character ─┐
├── Options phase → render() called once │ Canvas 2D API
├── Countdown phase → render() called per tick │ (1080 × 1920)
└── Answer phase → render() called once ─┘
│
▼
canvas.captureStream(30)
(live 30 FPS video track)
The render() function draws a complete frame on every call:
- White background fill
QUESTION N / TOTALlabel (top)- Question text with word-wrapping (
drawWrappedText) - Option boxes with
roundRect— highlighted green whenansIdxmatches - Countdown number (large red text) when
timer !== null
The audio engine is built entirely on the Web Audio API — no audio files are loaded.
AudioContext
├── createMediaStreamDestination() → streamDest (routed into the recording)
├── Tick buffer (pre-generated) → 100ms sine wave at 1100 Hz, decays in ~16ms
└── Silent oscillator (gain 0.00001) → keeps the audio stream alive during silence
Three synthesised layers fire simultaneously on each character:
| Layer | Frequencies | Duration | Purpose |
|---|---|---|---|
| Thock (body) | Bandpass @ 420 Hz, Q=1.2 | 60 ms | Low-mid punch of key bottom-out |
| Tick (snap) | High-pass @ 3500 Hz | 18 ms | Crisp plastic registration click |
| Clack (echo) | Bandpass @ 900 Hz, Q=2.0 | 12 ms, +4 ms delay | Subtle key-up rebound |
All three layers use white-noise bursts shaped by exponential gain envelopes.
Pre-generated sine wave at 1100 Hz with a sharp exponential decay (~16 ms). Fires once per countdown second and is routed to both the speaker and the recording stream.
canvas.captureStream(30) → VideoTrack ─┐
├── MediaStream → MediaRecorder
streamDest.stream → AudioTrack ─┘
│
ondataavailable
(every 100ms chunk)
│
recordedChunks[]
│
mediaRecorder.stop()
│
finalizeExport()
│
Blob → Object URL → <a>.click() → download
MIME type selection:
// Preferred: native MP4 (Chrome 130+)
"video/mp4;codecs=avc1";
// Fallback: WebM VP9 (all Chromium versions)
"video/webm;codecs=vp9";Wake lock: navigator.wakeLock.request('screen') is called at the start of generation and re-acquired on visibilitychange events to prevent the screen from sleeping mid-recording.
User pastes quizData
│
▼
startEngine()
│
├── eval() parses quizData array
├── initAudioEngine() → AudioContext + streamDest
├── canvas.captureStream(30) → videoStream
├── MediaStream(video + audio tracks)
├── MediaRecorder.start(100ms)
│
▼
startQuizProcess() [async loop]
│
├── render() per character / phase
├── triggerKeyClick() per character
├── triggerTick() per countdown tick
│
▼
mediaRecorder.stop()
│
▼
finalizeExport()
│
├── Blob ← recordedChunks[]
├── Object URL
└── Auto-download
| What to change | Where | Variable / value |
|---|---|---|
| Typing speed | startQuizProcess() |
await wait(50) — increase for slower typing |
| Click sound decay | triggerKeyClick() |
now + 0.03 on the gain ramp |
| Click sound volume | triggerKeyClick() |
thockGain.gain.setValueAtTime(0.35, ...) |
| Countdown beep pitch | initAudioEngine() |
1100 in Math.sin(2 * Math.PI * 1100 * t) |
| Answer highlight colour | render() |
"#10b981" (Tailwind emerald-500) |
| Video bitrate | startEngine() |
videoBitsPerSecond: 8000000 |
| Canvas resolution | HTML | width="1080" height="1920" on <canvas> |
| Options pause duration | startQuizProcess() |
await wait(1000) after options render |
| Answer reveal duration | startQuizProcess() |
await wait(2500) after answer highlight |