pokertools-arena 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app.js +244 -150
- package/dist/index.html +3 -3
- package/dist/pokertools-arena.html +245 -151
- package/docs/diagnostics/SUMMARY.md +1 -1
- package/docs/diagnostics/summary.json +1 -1
- package/docs/releases/RELEASE_NOTES.md +29 -0
- package/package.json +1 -1
- package/src/app.js +244 -160
- package/src/index.html +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| **Package** | `npx pokertools-arena` |
|
|
18
18
|
| **Runtime** | Node.js ≥ 24 for tooling; any modern browser for the app |
|
|
19
19
|
|
|
20
|
-
> **Current release: 0.
|
|
20
|
+
> **Current release: 0.8.0.** The tab-audio recording release: table recordings now capture the current tab's own audio from `getDisplayMedia()` and rebuild video frames through a timestamp-preserving `MediaStreamTrackProcessor` → `VideoFrame` pipeline, so the exported WebM keeps audio and picture on the same capture clock at up to 2560×1440.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
package/dist/app.js
CHANGED
|
@@ -115702,13 +115702,6 @@ function soundMaster() {
|
|
|
115702
115702
|
}
|
|
115703
115703
|
return soundMasterGain;
|
|
115704
115704
|
}
|
|
115705
|
-
function recordingAudioTrack() {
|
|
115706
|
-
const ctx = getAudioContext();
|
|
115707
|
-
if (!ctx || typeof ctx.createMediaStreamDestination !== "function") return null;
|
|
115708
|
-
const destination = ctx.createMediaStreamDestination();
|
|
115709
|
-
soundMaster()?.connect(destination);
|
|
115710
|
-
return { destination, track: destination.stream.getAudioTracks()[0] || null };
|
|
115711
|
-
}
|
|
115712
115705
|
function tone(freq, duration = 0.05, gain = 0.035, offset = 0, type = "sine") {
|
|
115713
115706
|
if (!soundEnabled) return;
|
|
115714
115707
|
const ctx = getAudioContext();
|
|
@@ -118000,26 +117993,26 @@ function openTests() {
|
|
|
118000
117993
|
}
|
|
118001
117994
|
var TABLE_RECORDING_CONFIG = Object.freeze({
|
|
118002
117995
|
frameRate: 30,
|
|
118003
|
-
maxWidth:
|
|
118004
|
-
maxHeight:
|
|
118005
|
-
minBitrate:
|
|
118006
|
-
maxBitrate:
|
|
118007
|
-
bitsPerPixelPerFrame: 0.
|
|
118008
|
-
audioBitsPerSecond:
|
|
117996
|
+
maxWidth: 2560,
|
|
117997
|
+
maxHeight: 1440,
|
|
117998
|
+
minBitrate: 1e7,
|
|
117999
|
+
maxBitrate: 28e6,
|
|
118000
|
+
bitsPerPixelPerFrame: 0.2,
|
|
118001
|
+
audioBitsPerSecond: 192e3,
|
|
118009
118002
|
background: "#07090b"
|
|
118010
118003
|
});
|
|
118011
|
-
function recorderMimeType() {
|
|
118004
|
+
function recorderMimeType(hasAudio = true) {
|
|
118012
118005
|
if (!globalThis.MediaRecorder) return "";
|
|
118013
|
-
|
|
118006
|
+
const candidates = hasAudio ? [
|
|
118014
118007
|
"video/webm;codecs=vp8,opus",
|
|
118015
118008
|
"video/webm;codecs=vp9,opus",
|
|
118009
|
+
"video/webm"
|
|
118010
|
+
] : [
|
|
118016
118011
|
"video/webm;codecs=vp8",
|
|
118017
118012
|
"video/webm;codecs=vp9",
|
|
118018
118013
|
"video/webm"
|
|
118019
|
-
]
|
|
118020
|
-
|
|
118021
|
-
}
|
|
118022
|
-
return "";
|
|
118014
|
+
];
|
|
118015
|
+
return candidates.find((type) => MediaRecorder.isTypeSupported?.(type)) || "";
|
|
118023
118016
|
}
|
|
118024
118017
|
function recordingVideoBitrate({ width, height, frameRate = TABLE_RECORDING_CONFIG.frameRate }) {
|
|
118025
118018
|
const pixelsPerSecond = Math.max(1, width) * Math.max(1, height) * Math.max(1, frameRate);
|
|
@@ -118037,7 +118030,7 @@ function setRecordButton(active, label = null) {
|
|
|
118037
118030
|
const text = $(".action-label", els.recordBtn);
|
|
118038
118031
|
if (icon) icon.textContent = active ? "\u25A0" : "\u25CF";
|
|
118039
118032
|
if (text) text.textContent = label || (active ? "Stop rec" : "Record");
|
|
118040
|
-
els.recordBtn.title = active ? "Stop table recording and save video" : "Record
|
|
118033
|
+
els.recordBtn.title = active ? "Stop table recording and save video" : "Record poker table with current-tab audio";
|
|
118041
118034
|
els.recordBtn.setAttribute("aria-label", els.recordBtn.title);
|
|
118042
118035
|
}
|
|
118043
118036
|
function saveRecordingBlob(blob) {
|
|
@@ -118068,72 +118061,26 @@ function fitRecordingSize(sourceWidth, sourceHeight) {
|
|
|
118068
118061
|
height: evenRecordingDimension(height * scale)
|
|
118069
118062
|
};
|
|
118070
118063
|
}
|
|
118071
|
-
function
|
|
118072
|
-
return new Promise((resolve, reject) => {
|
|
118073
|
-
let settled = false;
|
|
118074
|
-
const finish = (callback, value) => {
|
|
118075
|
-
if (settled) return;
|
|
118076
|
-
settled = true;
|
|
118077
|
-
clearTimeout(timeoutId);
|
|
118078
|
-
track?.removeEventListener?.("ended", onEnded);
|
|
118079
|
-
video.removeEventListener("loadedmetadata", onReady);
|
|
118080
|
-
video.removeEventListener("resize", onReady);
|
|
118081
|
-
callback(value);
|
|
118082
|
-
};
|
|
118083
|
-
const onReady = () => {
|
|
118084
|
-
if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
|
|
118085
|
-
};
|
|
118086
|
-
const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
|
|
118087
|
-
const timeoutId = setTimeout(
|
|
118088
|
-
() => finish(reject, new Error("Timed out waiting for the shared tab video")),
|
|
118089
|
-
8e3
|
|
118090
|
-
);
|
|
118091
|
-
track?.addEventListener?.("ended", onEnded, { once: true });
|
|
118092
|
-
video.addEventListener("loadedmetadata", onReady);
|
|
118093
|
-
video.addEventListener("resize", onReady);
|
|
118094
|
-
onReady();
|
|
118095
|
-
});
|
|
118096
|
-
}
|
|
118097
|
-
function createCaptureVideo(stream) {
|
|
118098
|
-
const video = document.createElement("video");
|
|
118099
|
-
video.muted = true;
|
|
118100
|
-
video.playsInline = true;
|
|
118101
|
-
video.autoplay = true;
|
|
118102
|
-
video.setAttribute("aria-hidden", "true");
|
|
118103
|
-
Object.assign(video.style, {
|
|
118104
|
-
position: "fixed",
|
|
118105
|
-
left: "0",
|
|
118106
|
-
top: "0",
|
|
118107
|
-
width: "2px",
|
|
118108
|
-
height: "2px",
|
|
118109
|
-
opacity: "0.001",
|
|
118110
|
-
pointerEvents: "none",
|
|
118111
|
-
zIndex: "2147483647"
|
|
118112
|
-
});
|
|
118113
|
-
video.srcObject = stream;
|
|
118114
|
-
document.body.append(video);
|
|
118115
|
-
return video;
|
|
118116
|
-
}
|
|
118117
|
-
function captureViewportMetrics(video) {
|
|
118064
|
+
function captureViewportMetrics(sourceWidth, sourceHeight) {
|
|
118118
118065
|
const viewport = globalThis.visualViewport;
|
|
118119
118066
|
const cssWidth = Math.max(1, viewport?.width || document.documentElement.clientWidth || innerWidth || 1);
|
|
118120
118067
|
const cssHeight = Math.max(1, viewport?.height || document.documentElement.clientHeight || innerHeight || 1);
|
|
118121
|
-
const
|
|
118122
|
-
const
|
|
118068
|
+
const width = Math.max(2, Number(sourceWidth) || cssWidth);
|
|
118069
|
+
const height = Math.max(2, Number(sourceHeight) || cssHeight);
|
|
118123
118070
|
return {
|
|
118124
118071
|
cssWidth,
|
|
118125
118072
|
cssHeight,
|
|
118126
|
-
sourceWidth,
|
|
118127
|
-
sourceHeight,
|
|
118128
|
-
scaleX:
|
|
118129
|
-
scaleY:
|
|
118073
|
+
sourceWidth: width,
|
|
118074
|
+
sourceHeight: height,
|
|
118075
|
+
scaleX: width / cssWidth,
|
|
118076
|
+
scaleY: height / cssHeight,
|
|
118130
118077
|
offsetX: viewport?.offsetLeft || 0,
|
|
118131
118078
|
offsetY: viewport?.offsetTop || 0
|
|
118132
118079
|
};
|
|
118133
118080
|
}
|
|
118134
|
-
function tableCropSourceRect(
|
|
118081
|
+
function tableCropSourceRect(sourceWidth, sourceHeight) {
|
|
118135
118082
|
const rect = els.pokerTable.getBoundingClientRect();
|
|
118136
|
-
const metrics = captureViewportMetrics(
|
|
118083
|
+
const metrics = captureViewportMetrics(sourceWidth, sourceHeight);
|
|
118137
118084
|
const leftCss = rect.left - metrics.offsetX;
|
|
118138
118085
|
const topCss = rect.top - metrics.offsetY;
|
|
118139
118086
|
const sx = clamp(Math.round(leftCss * metrics.scaleX), 0, Math.max(0, metrics.sourceWidth - 2));
|
|
@@ -118142,73 +118089,197 @@ function tableCropSourceRect(video) {
|
|
|
118142
118089
|
const sh = clamp(Math.round(rect.height * metrics.scaleY), 2, metrics.sourceHeight - sy);
|
|
118143
118090
|
return { sx, sy, sw, sh };
|
|
118144
118091
|
}
|
|
118145
|
-
function createTableRecordingCanvas(
|
|
118146
|
-
const source = tableCropSourceRect(
|
|
118092
|
+
function createTableRecordingCanvas(sourceWidth, sourceHeight) {
|
|
118093
|
+
const source = tableCropSourceRect(sourceWidth, sourceHeight);
|
|
118147
118094
|
const output = fitRecordingSize(source.sw, source.sh);
|
|
118148
118095
|
const canvas = document.createElement("canvas");
|
|
118149
118096
|
canvas.width = output.width;
|
|
118150
118097
|
canvas.height = output.height;
|
|
118151
|
-
const ctx = canvas.getContext("2d", { alpha: false
|
|
118098
|
+
const ctx = canvas.getContext("2d", { alpha: false });
|
|
118152
118099
|
if (!ctx) throw new Error("Canvas video recording is not available in this browser");
|
|
118153
118100
|
ctx.imageSmoothingEnabled = true;
|
|
118154
118101
|
ctx.imageSmoothingQuality = "high";
|
|
118155
118102
|
return { canvas, ctx };
|
|
118156
118103
|
}
|
|
118157
|
-
function
|
|
118158
|
-
const
|
|
118159
|
-
|
|
118160
|
-
|
|
118161
|
-
return { stream, track };
|
|
118162
|
-
}
|
|
118163
|
-
function paintTableRecordingFrame(recording) {
|
|
118164
|
-
const { ctx, canvas, captureVideo } = recording;
|
|
118165
|
-
if (!ctx || !canvas || !captureVideo || captureVideo.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return false;
|
|
118166
|
-
const source = tableCropSourceRect(captureVideo);
|
|
118167
|
-
ctx.save();
|
|
118104
|
+
function paintTableRecordingFrame(recording, source, sourceWidth, sourceHeight) {
|
|
118105
|
+
const { ctx, canvas } = recording;
|
|
118106
|
+
if (!ctx || !canvas || !source) return false;
|
|
118107
|
+
const crop = tableCropSourceRect(sourceWidth, sourceHeight);
|
|
118168
118108
|
ctx.globalCompositeOperation = "copy";
|
|
118169
118109
|
ctx.fillStyle = TABLE_RECORDING_CONFIG.background;
|
|
118170
118110
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
118171
118111
|
try {
|
|
118172
118112
|
ctx.drawImage(
|
|
118173
|
-
|
|
118174
|
-
|
|
118175
|
-
|
|
118176
|
-
|
|
118177
|
-
|
|
118113
|
+
source,
|
|
118114
|
+
crop.sx,
|
|
118115
|
+
crop.sy,
|
|
118116
|
+
crop.sw,
|
|
118117
|
+
crop.sh,
|
|
118178
118118
|
0,
|
|
118179
118119
|
0,
|
|
118180
118120
|
canvas.width,
|
|
118181
118121
|
canvas.height
|
|
118182
118122
|
);
|
|
118123
|
+
return true;
|
|
118183
118124
|
} catch {
|
|
118184
|
-
ctx.restore();
|
|
118185
118125
|
return false;
|
|
118126
|
+
} finally {
|
|
118127
|
+
ctx.globalCompositeOperation = "source-over";
|
|
118186
118128
|
}
|
|
118187
|
-
ctx.restore();
|
|
118188
|
-
return true;
|
|
118189
118129
|
}
|
|
118190
|
-
function
|
|
118130
|
+
function createCaptureVideo(stream) {
|
|
118131
|
+
const video = document.createElement("video");
|
|
118132
|
+
video.muted = true;
|
|
118133
|
+
video.playsInline = true;
|
|
118134
|
+
video.autoplay = true;
|
|
118135
|
+
video.setAttribute("aria-hidden", "true");
|
|
118136
|
+
Object.assign(video.style, {
|
|
118137
|
+
position: "fixed",
|
|
118138
|
+
inset: "0 auto auto 0",
|
|
118139
|
+
width: "2px",
|
|
118140
|
+
height: "2px",
|
|
118141
|
+
opacity: "0.001",
|
|
118142
|
+
pointerEvents: "none",
|
|
118143
|
+
zIndex: "-1"
|
|
118144
|
+
});
|
|
118145
|
+
video.srcObject = stream;
|
|
118146
|
+
document.body.append(video);
|
|
118147
|
+
return video;
|
|
118148
|
+
}
|
|
118149
|
+
function waitForCaptureVideo(video, track) {
|
|
118150
|
+
return new Promise((resolve, reject) => {
|
|
118151
|
+
let settled = false;
|
|
118152
|
+
const finish = (callback, value) => {
|
|
118153
|
+
if (settled) return;
|
|
118154
|
+
settled = true;
|
|
118155
|
+
clearTimeout(timeoutId);
|
|
118156
|
+
track?.removeEventListener?.("ended", onEnded);
|
|
118157
|
+
video.removeEventListener("loadedmetadata", onReady);
|
|
118158
|
+
video.removeEventListener("resize", onReady);
|
|
118159
|
+
callback(value);
|
|
118160
|
+
};
|
|
118161
|
+
const onReady = () => {
|
|
118162
|
+
if (video.videoWidth > 0 && video.videoHeight > 0) finish(resolve);
|
|
118163
|
+
};
|
|
118164
|
+
const onEnded = () => finish(reject, new Error("Screen sharing ended before recording started"));
|
|
118165
|
+
const timeoutId = setTimeout(
|
|
118166
|
+
() => finish(reject, new Error("Timed out waiting for the shared tab video")),
|
|
118167
|
+
8e3
|
|
118168
|
+
);
|
|
118169
|
+
track?.addEventListener?.("ended", onEnded, { once: true });
|
|
118170
|
+
video.addEventListener("loadedmetadata", onReady);
|
|
118171
|
+
video.addEventListener("resize", onReady);
|
|
118172
|
+
onReady();
|
|
118173
|
+
});
|
|
118174
|
+
}
|
|
118175
|
+
function supportsTimestampedVideoPipeline() {
|
|
118176
|
+
return typeof globalThis.MediaStreamTrackProcessor === "function" && typeof globalThis.MediaStreamTrackGenerator === "function" && typeof globalThis.VideoFrame === "function";
|
|
118177
|
+
}
|
|
118178
|
+
function createTimestampedVideoPipeline(recording) {
|
|
118179
|
+
if (!supportsTimestampedVideoPipeline()) return null;
|
|
118180
|
+
try {
|
|
118181
|
+
const processor = new MediaStreamTrackProcessor({ track: recording.captureTrack });
|
|
118182
|
+
const generator = new MediaStreamTrackGenerator({ kind: "video" });
|
|
118183
|
+
const reader = processor.readable.getReader();
|
|
118184
|
+
const writer = generator.writable.getWriter();
|
|
118185
|
+
generator.contentHint = "detail";
|
|
118186
|
+
recording.processorReader = reader;
|
|
118187
|
+
recording.generatorWriter = writer;
|
|
118188
|
+
recording.outputVideoTrack = generator;
|
|
118189
|
+
return generator;
|
|
118190
|
+
} catch {
|
|
118191
|
+
recording.processorReader = null;
|
|
118192
|
+
recording.generatorWriter = null;
|
|
118193
|
+
recording.outputVideoTrack = null;
|
|
118194
|
+
return null;
|
|
118195
|
+
}
|
|
118196
|
+
}
|
|
118197
|
+
function startTimestampedVideoPump(recording) {
|
|
118198
|
+
const reader = recording.processorReader;
|
|
118199
|
+
const writer = recording.generatorWriter;
|
|
118200
|
+
if (!reader || !writer) return;
|
|
118201
|
+
const fallbackDuration = Math.round(1e6 / TABLE_RECORDING_CONFIG.frameRate);
|
|
118202
|
+
recording.videoPumpPromise = (async () => {
|
|
118203
|
+
try {
|
|
118204
|
+
while (!recording.stopping) {
|
|
118205
|
+
const { value: frame, done } = await reader.read();
|
|
118206
|
+
if (done || !frame) break;
|
|
118207
|
+
try {
|
|
118208
|
+
const width = frame.displayWidth || frame.codedWidth || recording.sourceWidth;
|
|
118209
|
+
const height = frame.displayHeight || frame.codedHeight || recording.sourceHeight;
|
|
118210
|
+
if (!paintTableRecordingFrame(recording, frame, width, height)) continue;
|
|
118211
|
+
if (writer.desiredSize != null && writer.desiredSize <= 0) continue;
|
|
118212
|
+
const timestamp = Number.isFinite(frame.timestamp) ? frame.timestamp : Math.round(performance.now() * 1e3);
|
|
118213
|
+
const duration = Number.isFinite(frame.duration) && frame.duration > 0 ? frame.duration : fallbackDuration;
|
|
118214
|
+
const outputFrame = new VideoFrame(recording.canvas, { timestamp, duration });
|
|
118215
|
+
try {
|
|
118216
|
+
await writer.write(outputFrame);
|
|
118217
|
+
} finally {
|
|
118218
|
+
outputFrame.close();
|
|
118219
|
+
}
|
|
118220
|
+
} finally {
|
|
118221
|
+
frame.close();
|
|
118222
|
+
}
|
|
118223
|
+
}
|
|
118224
|
+
} catch (error) {
|
|
118225
|
+
if (!recording.stopping && tableRecording === recording) {
|
|
118226
|
+
recording.pipelineError = error;
|
|
118227
|
+
if (recording.recorder?.state !== "inactive") recording.recorder.stop();
|
|
118228
|
+
}
|
|
118229
|
+
}
|
|
118230
|
+
})();
|
|
118231
|
+
}
|
|
118232
|
+
function createFallbackCanvasPipeline(recording) {
|
|
118233
|
+
const stream = recording.canvas.captureStream(TABLE_RECORDING_CONFIG.frameRate);
|
|
118234
|
+
const track = stream.getVideoTracks()[0];
|
|
118235
|
+
if (!track) throw new Error("Could not create the table recording video track");
|
|
118236
|
+
track.contentHint = "detail";
|
|
118237
|
+
recording.canvasStream = stream;
|
|
118238
|
+
recording.outputVideoTrack = track;
|
|
118239
|
+
return track;
|
|
118240
|
+
}
|
|
118241
|
+
function startFallbackVideoPump(recording) {
|
|
118191
118242
|
const frameInterval = 1e3 / TABLE_RECORDING_CONFIG.frameRate;
|
|
118192
118243
|
let previousPaint = -frameInterval;
|
|
118193
118244
|
const loop = (timestamp) => {
|
|
118194
118245
|
if (recording.stopping || tableRecording !== recording) return;
|
|
118195
118246
|
if (timestamp - previousPaint >= frameInterval - 1) {
|
|
118196
|
-
|
|
118247
|
+
const video2 = recording.captureVideo;
|
|
118248
|
+
if (video2?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
118249
|
+
paintTableRecordingFrame(recording, video2, video2.videoWidth, video2.videoHeight);
|
|
118250
|
+
}
|
|
118197
118251
|
previousPaint = timestamp;
|
|
118198
118252
|
}
|
|
118199
118253
|
recording.animationFrameId = requestAnimationFrame(loop);
|
|
118200
118254
|
};
|
|
118201
|
-
|
|
118255
|
+
const video = recording.captureVideo;
|
|
118256
|
+
if (video?.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
118257
|
+
paintTableRecordingFrame(recording, video, video.videoWidth, video.videoHeight);
|
|
118258
|
+
}
|
|
118202
118259
|
recording.animationFrameId = requestAnimationFrame(loop);
|
|
118203
118260
|
}
|
|
118204
|
-
function
|
|
118261
|
+
function startTableRecordingVideo(recording) {
|
|
118262
|
+
if (recording.processorReader) startTimestampedVideoPump(recording);
|
|
118263
|
+
else startFallbackVideoPump(recording);
|
|
118264
|
+
}
|
|
118265
|
+
function stopTableRecordingVideo(recording) {
|
|
118205
118266
|
if (!recording) return;
|
|
118206
118267
|
if (recording.animationFrameId != null) cancelAnimationFrame(recording.animationFrameId);
|
|
118207
118268
|
recording.animationFrameId = null;
|
|
118269
|
+
try {
|
|
118270
|
+
recording.processorReader?.cancel?.();
|
|
118271
|
+
} catch {
|
|
118272
|
+
}
|
|
118273
|
+
try {
|
|
118274
|
+
recording.generatorWriter?.close?.();
|
|
118275
|
+
} catch {
|
|
118276
|
+
}
|
|
118277
|
+
recording.processorReader = null;
|
|
118278
|
+
recording.generatorWriter = null;
|
|
118208
118279
|
}
|
|
118209
|
-
function disposeTableRecording(recording
|
|
118280
|
+
function disposeTableRecording(recording) {
|
|
118210
118281
|
if (!recording) return;
|
|
118211
|
-
|
|
118282
|
+
stopTableRecordingVideo(recording);
|
|
118212
118283
|
try {
|
|
118213
118284
|
recording.captureVideo?.pause?.();
|
|
118214
118285
|
} catch {
|
|
@@ -118217,28 +118288,28 @@ function disposeTableRecording(recording, { stopCapture = true, stopCanvas = tru
|
|
|
118217
118288
|
recording.captureVideo.srcObject = null;
|
|
118218
118289
|
recording.captureVideo.remove();
|
|
118219
118290
|
}
|
|
118220
|
-
|
|
118221
|
-
if (stopCapture) recording.captureStream?.getTracks?.().forEach((track) => track.stop());
|
|
118291
|
+
recording.canvasStream?.getTracks?.().forEach((track) => track.stop());
|
|
118222
118292
|
try {
|
|
118223
|
-
|
|
118224
|
-
} catch {
|
|
118225
|
-
}
|
|
118226
|
-
try {
|
|
118227
|
-
recording.audioTrack?.stop?.();
|
|
118293
|
+
recording.outputVideoTrack?.stop?.();
|
|
118228
118294
|
} catch {
|
|
118229
118295
|
}
|
|
118296
|
+
recording.captureStream?.getTracks?.().forEach((track) => track.stop());
|
|
118230
118297
|
recording.canvas?.remove?.();
|
|
118231
118298
|
}
|
|
118232
118299
|
function finalizeTableRecording(recording, mimeType) {
|
|
118233
118300
|
const blob = new Blob(recording.chunks, { type: recording.recorder.mimeType || mimeType || "video/webm" });
|
|
118301
|
+
const error = recording.pipelineError;
|
|
118234
118302
|
disposeTableRecording(recording);
|
|
118235
118303
|
if (tableRecording === recording) tableRecording = null;
|
|
118236
118304
|
setRecordButton(false);
|
|
118237
118305
|
if (blob.size) saveRecordingBlob(blob);
|
|
118306
|
+
if (error) showActionToast(`Recording stopped: ${summarizeError(error)}`, "fold");
|
|
118238
118307
|
}
|
|
118239
118308
|
async function startTableRecording() {
|
|
118240
118309
|
if (tableRecording || !els.pokerTable) return;
|
|
118241
|
-
|
|
118310
|
+
const canTimestampVideo = supportsTimestampedVideoPipeline();
|
|
118311
|
+
const canCanvasVideo = typeof HTMLCanvasElement.prototype.captureStream === "function";
|
|
118312
|
+
if (!navigator.mediaDevices?.getDisplayMedia || !globalThis.MediaRecorder || !canTimestampVideo && !canCanvasVideo) {
|
|
118242
118313
|
showActionToast("Table recording is not supported in this browser", "fold");
|
|
118243
118314
|
return;
|
|
118244
118315
|
}
|
|
@@ -118249,12 +118320,15 @@ async function startTableRecording() {
|
|
|
118249
118320
|
captureStream = await navigator.mediaDevices.getDisplayMedia({
|
|
118250
118321
|
video: {
|
|
118251
118322
|
displaySurface: "browser",
|
|
118252
|
-
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
|
|
118323
|
+
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate },
|
|
118324
|
+
width: { ideal: TABLE_RECORDING_CONFIG.maxWidth },
|
|
118325
|
+
height: { ideal: TABLE_RECORDING_CONFIG.maxHeight }
|
|
118253
118326
|
},
|
|
118254
|
-
audio:
|
|
118327
|
+
audio: true,
|
|
118255
118328
|
preferCurrentTab: true,
|
|
118256
118329
|
selfBrowserSurface: "include",
|
|
118257
|
-
surfaceSwitching: "exclude"
|
|
118330
|
+
surfaceSwitching: "exclude",
|
|
118331
|
+
systemAudio: "exclude"
|
|
118258
118332
|
});
|
|
118259
118333
|
const captureTrack = captureStream.getVideoTracks()[0];
|
|
118260
118334
|
if (!captureTrack) throw new Error("No video track was shared");
|
|
@@ -118262,48 +118336,74 @@ async function startTableRecording() {
|
|
|
118262
118336
|
if (displaySurface && displaySurface !== "browser") {
|
|
118263
118337
|
throw new Error("Please share this browser tab, not a window or the entire screen.");
|
|
118264
118338
|
}
|
|
118339
|
+
try {
|
|
118340
|
+
captureTrack.contentHint = "detail";
|
|
118341
|
+
} catch {
|
|
118342
|
+
}
|
|
118265
118343
|
try {
|
|
118266
118344
|
await captureTrack.applyConstraints?.({
|
|
118267
118345
|
frameRate: { ideal: TABLE_RECORDING_CONFIG.frameRate, max: TABLE_RECORDING_CONFIG.frameRate }
|
|
118268
118346
|
});
|
|
118269
118347
|
} catch {
|
|
118270
118348
|
}
|
|
118349
|
+
const audioTrack = captureStream.getAudioTracks()[0] || null;
|
|
118350
|
+
if (audioTrack?.applyConstraints) {
|
|
118351
|
+
try {
|
|
118352
|
+
await audioTrack.applyConstraints({ suppressLocalAudioPlayback: false });
|
|
118353
|
+
} catch {
|
|
118354
|
+
}
|
|
118355
|
+
}
|
|
118271
118356
|
captureVideo = createCaptureVideo(captureStream);
|
|
118272
118357
|
await captureVideo.play().catch(() => {
|
|
118273
118358
|
});
|
|
118274
118359
|
await waitForCaptureVideo(captureVideo, captureTrack);
|
|
118275
|
-
const
|
|
118276
|
-
const
|
|
118277
|
-
const {
|
|
118278
|
-
try {
|
|
118279
|
-
canvasTrack.contentHint = "detail";
|
|
118280
|
-
} catch {
|
|
118281
|
-
}
|
|
118282
|
-
const recordingAudio = recordingAudioTrack();
|
|
118283
|
-
const audioTrack = recordingAudio?.track || null;
|
|
118284
|
-
const recorderStream = audioTrack ? new MediaStream([canvasTrack, audioTrack]) : canvasStream;
|
|
118360
|
+
const sourceWidth = Math.max(2, captureVideo.videoWidth || innerWidth || 2);
|
|
118361
|
+
const sourceHeight = Math.max(2, captureVideo.videoHeight || innerHeight || 2);
|
|
118362
|
+
const { canvas, ctx } = createTableRecordingCanvas(sourceWidth, sourceHeight);
|
|
118285
118363
|
recording = {
|
|
118286
118364
|
recorder: null,
|
|
118365
|
+
recorderStream: null,
|
|
118287
118366
|
captureStream,
|
|
118288
118367
|
captureTrack,
|
|
118289
118368
|
captureVideo,
|
|
118290
118369
|
canvas,
|
|
118291
118370
|
ctx,
|
|
118292
|
-
canvasStream,
|
|
118293
|
-
|
|
118371
|
+
canvasStream: null,
|
|
118372
|
+
outputVideoTrack: null,
|
|
118373
|
+
processorReader: null,
|
|
118374
|
+
generatorWriter: null,
|
|
118375
|
+
videoPumpPromise: null,
|
|
118376
|
+
animationFrameId: null,
|
|
118377
|
+
sourceWidth,
|
|
118378
|
+
sourceHeight,
|
|
118294
118379
|
audioTrack,
|
|
118295
|
-
audioDestination: recordingAudio?.destination || null,
|
|
118296
118380
|
chunks: [],
|
|
118297
118381
|
stopping: false,
|
|
118298
|
-
|
|
118382
|
+
pipelineError: null
|
|
118299
118383
|
};
|
|
118300
|
-
|
|
118384
|
+
let outputVideoTrack = canTimestampVideo ? createTimestampedVideoPipeline(recording) : null;
|
|
118385
|
+
if (outputVideoTrack) {
|
|
118386
|
+
captureVideo.pause();
|
|
118387
|
+
captureVideo.srcObject = null;
|
|
118388
|
+
captureVideo.remove();
|
|
118389
|
+
captureVideo = null;
|
|
118390
|
+
recording.captureVideo = null;
|
|
118391
|
+
} else {
|
|
118392
|
+
if (!canCanvasVideo) throw new Error("This browser cannot create a recording video track");
|
|
118393
|
+
outputVideoTrack = createFallbackCanvasPipeline(recording);
|
|
118394
|
+
}
|
|
118395
|
+
const tracks = [outputVideoTrack];
|
|
118396
|
+
if (audioTrack) tracks.push(audioTrack);
|
|
118397
|
+
const recorderStream = new MediaStream(tracks);
|
|
118398
|
+
recording.recorderStream = recorderStream;
|
|
118399
|
+
const mimeType = recorderMimeType(Boolean(audioTrack));
|
|
118301
118400
|
const videoBitsPerSecond = recordingVideoBitrate({
|
|
118302
118401
|
width: canvas.width,
|
|
118303
118402
|
height: canvas.height,
|
|
118304
118403
|
frameRate: TABLE_RECORDING_CONFIG.frameRate
|
|
118305
118404
|
});
|
|
118306
|
-
const options = { videoBitsPerSecond
|
|
118405
|
+
const options = { videoBitsPerSecond };
|
|
118406
|
+
if (audioTrack) options.audioBitsPerSecond = TABLE_RECORDING_CONFIG.audioBitsPerSecond;
|
|
118307
118407
|
if (mimeType) options.mimeType = mimeType;
|
|
118308
118408
|
const recorder = new MediaRecorder(recorderStream, options);
|
|
118309
118409
|
recording.recorder = recorder;
|
|
@@ -118314,29 +118414,21 @@ async function startTableRecording() {
|
|
|
118314
118414
|
recorder.addEventListener("stop", () => finalizeTableRecording(recording, mimeType), { once: true });
|
|
118315
118415
|
recorder.addEventListener("error", (event) => {
|
|
118316
118416
|
if (tableRecording !== recording) return;
|
|
118417
|
+
recording.pipelineError = event.error || new Error("Recording failed");
|
|
118317
118418
|
recording.stopping = true;
|
|
118318
|
-
|
|
118319
|
-
showActionToast(summarizeError(event.error || new Error("Recording failed")), "fold");
|
|
118419
|
+
stopTableRecordingVideo(recording);
|
|
118320
118420
|
if (recorder.state !== "inactive") recorder.stop();
|
|
118321
|
-
else
|
|
118322
|
-
disposeTableRecording(recording);
|
|
118323
|
-
tableRecording = null;
|
|
118324
|
-
setRecordButton(false);
|
|
118325
|
-
}
|
|
118421
|
+
else finalizeTableRecording(recording, mimeType);
|
|
118326
118422
|
});
|
|
118327
118423
|
captureTrack.addEventListener("ended", () => {
|
|
118328
|
-
if (tableRecording === recording && recorder.state !== "inactive")
|
|
118329
|
-
recording.stopping = true;
|
|
118330
|
-
stopTableRecordingPainter(recording);
|
|
118331
|
-
recorder.stop();
|
|
118332
|
-
}
|
|
118424
|
+
if (tableRecording === recording && recorder.state !== "inactive") stopTableRecording();
|
|
118333
118425
|
}, { once: true });
|
|
118334
|
-
|
|
118335
|
-
|
|
118426
|
+
recorder.start(500);
|
|
118427
|
+
startTableRecordingVideo(recording);
|
|
118336
118428
|
setRecordButton(true);
|
|
118337
118429
|
showActionToast(
|
|
118338
|
-
`Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps${audioTrack ? "
|
|
118339
|
-
"check"
|
|
118430
|
+
`Recording \xB7 ${canvas.width}\xD7${canvas.height} \xB7 ${TABLE_RECORDING_CONFIG.frameRate} fps \xB7 ${audioTrack ? "tab audio" : "no tab audio"}`,
|
|
118431
|
+
audioTrack ? "check" : "fold"
|
|
118340
118432
|
);
|
|
118341
118433
|
} catch (err) {
|
|
118342
118434
|
if (recording) disposeTableRecording(recording);
|
|
@@ -118356,14 +118448,16 @@ function stopTableRecording() {
|
|
|
118356
118448
|
const recording = tableRecording;
|
|
118357
118449
|
if (!recording || recording.stopping) return;
|
|
118358
118450
|
recording.stopping = true;
|
|
118359
|
-
|
|
118451
|
+
stopTableRecordingVideo(recording);
|
|
118360
118452
|
if (recording.recorder.state !== "inactive") {
|
|
118453
|
+
try {
|
|
118454
|
+
recording.recorder.requestData?.();
|
|
118455
|
+
} catch {
|
|
118456
|
+
}
|
|
118361
118457
|
recording.recorder.stop();
|
|
118362
118458
|
return;
|
|
118363
118459
|
}
|
|
118364
|
-
|
|
118365
|
-
if (tableRecording === recording) tableRecording = null;
|
|
118366
|
-
setRecordButton(false);
|
|
118460
|
+
finalizeTableRecording(recording, recording.recorder.mimeType);
|
|
118367
118461
|
}
|
|
118368
118462
|
function modelForPlayerId(playerId) {
|
|
118369
118463
|
return currentState?.config?.players?.find?.((p) => p.id === playerId)?.model || "";
|
package/dist/index.html
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
</script>
|
|
55
55
|
|
|
56
|
-
<link rel="stylesheet" href="./styles.css?v=0.6.
|
|
56
|
+
<link rel="stylesheet" href="./styles.css?v=0.6.6-tab-avsync-quality" />
|
|
57
57
|
</head>
|
|
58
58
|
<body>
|
|
59
59
|
<div class="app-shell">
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
<div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press ▶</div>
|
|
129
129
|
<div class="arena-head-actions">
|
|
130
130
|
<button id="soundBtn" class="arena-icon-button sound-button" type="button" aria-pressed="false" title="Enable table sounds" aria-label="Enable table sounds"><span class="action-icon">♪</span></button>
|
|
131
|
-
<button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record
|
|
131
|
+
<button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record poker table with current-tab audio" aria-label="Record poker table with current-tab audio"><span class="action-icon">●</span></button>
|
|
132
132
|
</div>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
@@ -391,6 +391,6 @@
|
|
|
391
391
|
</template>
|
|
392
392
|
|
|
393
393
|
<script src="./arena-env.js"></script>
|
|
394
|
-
<script type="module" src="./app.js?v=0.6.
|
|
394
|
+
<script type="module" src="./app.js?v=0.6.6-tab-avsync-quality"></script>
|
|
395
395
|
</body>
|
|
396
396
|
</html>
|