2025-03-16 17:45:29 +01:00
|
|
|
import { gameCanvas } from "./render";
|
|
|
|
import { max_levels } from "./game_utils";
|
|
|
|
import { getAudioRecordingTrack } from "./sounds";
|
|
|
|
import { t } from "./i18n/i18n";
|
|
|
|
import { GameState } from "./types";
|
|
|
|
import { isOptionOn } from "./options";
|
2025-03-16 14:29:14 +01:00
|
|
|
|
|
|
|
let mediaRecorder: MediaRecorder | null,
|
2025-03-16 17:45:29 +01:00
|
|
|
captureStream: MediaStream,
|
|
|
|
captureTrack: CanvasCaptureMediaStreamTrack,
|
|
|
|
recordCanvas: HTMLCanvasElement,
|
|
|
|
recordCanvasCtx: CanvasRenderingContext2D;
|
|
|
|
|
|
|
|
export function recordOneFrame(gameState: GameState) {
|
|
|
|
if (!isOptionOn("record")) {
|
|
|
|
return;
|
|
|
|
}
|
2025-03-16 20:11:08 +01:00
|
|
|
// if (!gameState.running) return;
|
2025-03-16 17:45:29 +01:00
|
|
|
if (!captureStream) return;
|
|
|
|
drawMainCanvasOnSmallCanvas(gameState);
|
|
|
|
if (captureTrack?.requestFrame) {
|
|
|
|
captureTrack?.requestFrame();
|
|
|
|
} else if (captureStream?.requestFrame) {
|
|
|
|
captureStream.requestFrame();
|
|
|
|
}
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
2025-03-16 17:45:29 +01:00
|
|
|
export function drawMainCanvasOnSmallCanvas(gameState: GameState) {
|
|
|
|
if (!recordCanvasCtx) return;
|
|
|
|
recordCanvasCtx.drawImage(
|
|
|
|
gameCanvas,
|
|
|
|
gameState.offsetXRoundedDown,
|
|
|
|
0,
|
|
|
|
gameState.gameZoneWidthRoundedUp,
|
|
|
|
gameState.gameZoneHeight,
|
|
|
|
0,
|
|
|
|
0,
|
|
|
|
recordCanvas.width,
|
|
|
|
recordCanvas.height,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Here we don't use drawText as we don't want to cache a picture for each distinct value of score
|
|
|
|
recordCanvasCtx.fillStyle = "#FFF";
|
|
|
|
recordCanvasCtx.textBaseline = "top";
|
|
|
|
recordCanvasCtx.font = "12px monospace";
|
|
|
|
recordCanvasCtx.textAlign = "right";
|
|
|
|
recordCanvasCtx.fillText(
|
|
|
|
gameState.score.toString(),
|
|
|
|
recordCanvas.width - 12,
|
|
|
|
12,
|
|
|
|
);
|
|
|
|
|
|
|
|
recordCanvasCtx.textAlign = "left";
|
|
|
|
recordCanvasCtx.fillText(
|
|
|
|
"Level " + (gameState.currentLevel + 1) + "/" + max_levels(gameState),
|
|
|
|
12,
|
|
|
|
12,
|
|
|
|
);
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
2025-03-16 17:45:29 +01:00
|
|
|
export function startRecordingGame(gameState: GameState) {
|
|
|
|
if (!isOptionOn("record")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (mediaRecorder) return;
|
|
|
|
if (!recordCanvas) {
|
|
|
|
// Smaller canvas with fewer details
|
|
|
|
recordCanvas = document.createElement("canvas");
|
|
|
|
recordCanvasCtx = recordCanvas.getContext("2d", {
|
|
|
|
antialias: false,
|
|
|
|
alpha: false,
|
|
|
|
}) as CanvasRenderingContext2D;
|
|
|
|
|
|
|
|
captureStream = recordCanvas.captureStream(0);
|
|
|
|
captureTrack =
|
|
|
|
captureStream.getVideoTracks()[0] as CanvasCaptureMediaStreamTrack;
|
|
|
|
|
|
|
|
const track = getAudioRecordingTrack();
|
|
|
|
if (track) {
|
|
|
|
captureStream.addTrack(track.stream.getAudioTracks()[0]);
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
2025-03-16 17:45:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
recordCanvas.width = gameState.gameZoneWidthRoundedUp;
|
|
|
|
recordCanvas.height = gameState.gameZoneHeight;
|
|
|
|
|
|
|
|
// drawMainCanvasOnSmallCanvas()
|
|
|
|
const recordedChunks: Blob[] = [];
|
|
|
|
|
|
|
|
const instance = new MediaRecorder(captureStream, {
|
|
|
|
videoBitsPerSecond: 3500000,
|
|
|
|
});
|
|
|
|
mediaRecorder = instance;
|
|
|
|
instance.start();
|
|
|
|
mediaRecorder.pause();
|
|
|
|
instance.ondataavailable = function (event) {
|
|
|
|
recordedChunks.push(event.data);
|
|
|
|
};
|
|
|
|
|
|
|
|
instance.onstop = async function () {
|
|
|
|
let targetDiv: HTMLElement | null;
|
|
|
|
let blob = new Blob(recordedChunks, { type: "video/webm" });
|
|
|
|
if (blob.size < 200000) return; // under 0.2MB, probably bugged out or pointlessly short
|
|
|
|
|
|
|
|
while (
|
|
|
|
!(targetDiv = document.getElementById("level-recording-container"))
|
|
|
|
) {
|
|
|
|
await new Promise((r) => setTimeout(r, 200));
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
2025-03-16 17:45:29 +01:00
|
|
|
const video = document.createElement("video");
|
|
|
|
video.autoplay = true;
|
|
|
|
video.controls = false;
|
|
|
|
video.disablePictureInPicture = true;
|
|
|
|
video.disableRemotePlayback = true;
|
|
|
|
video.width = recordCanvas.width;
|
|
|
|
video.height = recordCanvas.height;
|
|
|
|
video.loop = true;
|
|
|
|
video.muted = true;
|
|
|
|
video.playsInline = true;
|
2025-03-18 15:26:56 +01:00
|
|
|
|
2025-03-16 17:45:29 +01:00
|
|
|
video.src = URL.createObjectURL(blob);
|
2025-03-18 15:26:56 +01:00
|
|
|
targetDiv.appendChild(video);
|
2025-03-16 17:45:29 +01:00
|
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
a.download = captureFileName("webm");
|
|
|
|
a.target = "_blank";
|
2025-03-18 15:26:56 +01:00
|
|
|
if (window.location.href.endsWith("index.html?isInWebView=true")) {
|
|
|
|
a.href = await blobToBase64(blob);
|
|
|
|
} else {
|
|
|
|
a.href = video.src;
|
|
|
|
}
|
|
|
|
|
2025-03-16 17:45:29 +01:00
|
|
|
a.textContent = t("main_menu.record_download", {
|
|
|
|
size: (blob.size / 1000000).toFixed(2),
|
2025-03-16 14:29:14 +01:00
|
|
|
});
|
2025-03-16 17:45:29 +01:00
|
|
|
targetDiv.appendChild(a);
|
|
|
|
};
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
2025-03-18 15:26:56 +01:00
|
|
|
function blobToBase64(blob: Blob): Promise<string> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let reader = new FileReader();
|
|
|
|
|
|
|
|
reader.onload = function () {
|
|
|
|
resolve(reader.result);
|
|
|
|
};
|
|
|
|
reader.onerror = function (e) {
|
|
|
|
console.error(e);
|
|
|
|
reject(new Error("Failed to readAsDataURL of the video "));
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.readAsDataURL(blob);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-03-16 17:45:29 +01:00
|
|
|
export function pauseRecording() {
|
|
|
|
if (!isOptionOn("record")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (mediaRecorder?.state === "recording") {
|
|
|
|
mediaRecorder?.pause();
|
|
|
|
}
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function resumeRecording() {
|
2025-03-16 17:45:29 +01:00
|
|
|
if (!isOptionOn("record")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (mediaRecorder?.state === "paused") {
|
|
|
|
mediaRecorder.resume();
|
|
|
|
}
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function stopRecording() {
|
2025-03-16 17:45:29 +01:00
|
|
|
if (!isOptionOn("record")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!mediaRecorder) return;
|
|
|
|
mediaRecorder?.stop();
|
|
|
|
mediaRecorder = null;
|
2025-03-16 14:29:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function captureFileName(ext = "webm") {
|
2025-03-16 17:45:29 +01:00
|
|
|
return (
|
|
|
|
"breakout-71-capture-" +
|
|
|
|
new Date().toISOString().replace(/[^0-9\-]+/gi, "-") +
|
|
|
|
"." +
|
|
|
|
ext
|
|
|
|
);
|
|
|
|
}
|