Skip to content

Commit c56884c

Browse files
authored
Add progress bar for video encoding (#106)
* Add progress bar for video encoding * Remove debugging console.logs * Disable record button when encoding
1 parent 5f73647 commit c56884c

File tree

2 files changed

+87
-25
lines changed

2 files changed

+87
-25
lines changed

examples/assets/scripts/video-recorder-ui.mjs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { Script } from 'playcanvas';
33
export class VideoRecorderUI extends Script {
44
initialize() {
55
this.createUI();
6+
// Listen to video recording progress events
7+
this.app.on('encode:begin', this.onEncodeBegin, this);
8+
this.app.on('encode:progress', this.onEncodeProgress, this);
9+
this.app.on('encode:end', this.onEncodeEnd, this);
610
}
711

812
createUI() {
@@ -40,8 +44,8 @@ export class VideoRecorderUI extends Script {
4044

4145
const label = document.createElement('label');
4246
label.textContent = labelText;
43-
label.style.width = '100px'; // Fixed width for alignment
44-
label.style.color = '#fff'; // Very light grey
47+
label.style.width = '100px';
48+
label.style.color = '#fff';
4549

4650
const select = document.createElement('select');
4751
select.style.width = '75px';
@@ -101,6 +105,9 @@ export class VideoRecorderUI extends Script {
101105
button.style.width = '100%'; // Make button same width as rows
102106
button.style.fontSize = 'inherit';
103107

108+
// Store the button reference on the instance for later access.
109+
this.recordButton = button;
110+
104111
button.addEventListener('click', () => {
105112
const videoRecorder = this.entity.script.videoRecorder;
106113
if (videoRecorder.recording) {
@@ -135,8 +142,56 @@ export class VideoRecorderUI extends Script {
135142
}
136143
});
137144

145+
// Create a simple progress indicator (could be a div styled as a progress bar)
146+
this.progressBar = document.createElement('div');
147+
this.progressBar.style.height = '10px';
148+
this.progressBar.style.background = '#ccc';
149+
this.progressBar.style.width = '0%';
150+
this.progressBar.style.transition = 'width 0.2s';
151+
container.appendChild(this.progressBar);
152+
138153
container.appendChild(optionsContainer);
139154
container.appendChild(button);
140155
document.body.appendChild(container);
141156
}
157+
158+
onEncodeBegin() {
159+
// Show the progress bar and disable the Record/Stop button once encoding begins.
160+
this.progressBar.style.display = 'block';
161+
this.recordButton.disabled = true;
162+
}
163+
164+
onEncodeProgress(progress) {
165+
// progress is a value between 0 and 1
166+
// Update our progress bar width accordingly.
167+
const percent = Math.min(Math.max(progress * 100, 0), 100);
168+
this.progressBar.style.width = `${percent}%`;
169+
}
170+
171+
onEncodeEnd(buffer) {
172+
// Hide the progress bar and re-enable the Record/Stop button once encoding is complete.
173+
this.progressBar.style.display = 'none';
174+
this.recordButton.disabled = false;
175+
176+
// Download the recorded video.
177+
const blob = new Blob([buffer], { type: 'video/mp4' });
178+
this.downloadBlob(blob);
179+
}
180+
181+
/**
182+
* Download the recorded video.
183+
*
184+
* @param {Blob} blob - The recorded video blob.
185+
* @private
186+
*/
187+
downloadBlob(blob) {
188+
const url = URL.createObjectURL(blob);
189+
190+
const a = document.createElement('a');
191+
a.href = url;
192+
a.download = 'recording.mp4';
193+
a.click();
194+
195+
URL.revokeObjectURL(url);
196+
}
142197
}

examples/assets/scripts/video-recorder.mjs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ export class VideoRecorder extends Script {
5252
muxer = null;
5353

5454
/** @private */
55-
framesGenerated = 0;
55+
totalFrames = 0;
56+
57+
/** @private */
58+
framesEncoded = 0;
59+
60+
/** @private */
61+
framesEncodedAtFlush = 0;
5662

5763
/** @private */
5864
recording = false;
@@ -95,13 +101,13 @@ export class VideoRecorder extends Script {
95101

96102
captureFrame() {
97103
const frame = new VideoFrame(this.app.graphicsDevice.canvas, {
98-
timestamp: this.framesGenerated * 1e6 / this.frameRate,
104+
timestamp: this.totalFrames * 1e6 / this.frameRate,
99105
duration: 1e6 / this.frameRate
100106
});
101107
this.encoder.encode(frame);
102108
frame.close();
103109

104-
this.framesGenerated++;
110+
this.totalFrames++;
105111
}
106112

107113
/**
@@ -110,7 +116,9 @@ export class VideoRecorder extends Script {
110116
record() {
111117
if (this.recording) return;
112118
this.recording = true;
113-
this.framesGenerated = 0;
119+
this.totalFrames = 0;
120+
this.framesEncoded = 0;
121+
this.framesEncodedAtFlush = 0;
114122

115123
const { width, height } = this.getResolutionDimensions();
116124

@@ -128,7 +136,13 @@ export class VideoRecorder extends Script {
128136

129137
// Create video frame encoder
130138
this.encoder = new VideoEncoder({
131-
output: (chunk, meta) => this.muxer.addVideoChunk(chunk, meta),
139+
output: (chunk, meta) => {
140+
this.framesEncoded++;
141+
this.muxer.addVideoChunk(chunk, meta);
142+
if (!this.recording) {
143+
this.app.fire('encode:progress', (this.framesEncoded - this.framesEncodedAtFlush) / (this.totalFrames - this.framesEncodedAtFlush));
144+
}
145+
},
132146
error: e => console.error(e)
133147
});
134148

@@ -158,10 +172,17 @@ export class VideoRecorder extends Script {
158172
async stop() {
159173
if (!this.recording) return;
160174
this.recording = false;
175+
this.framesEncodedAtFlush = this.framesEncoded;
176+
177+
this.app.fire('encode:begin');
161178

162179
// Restore update function
163180
this.restoreUpdate();
164181

182+
// Disable auto render - this allows CPU/GPU resources to be directed towards encoding
183+
const originalAutoRender = this.app.autoRender;
184+
this.app.autoRender = false;
185+
165186
// Stop capturing frames
166187
this.app.off('frameend', this.captureFrame, this);
167188

@@ -175,30 +196,16 @@ export class VideoRecorder extends Script {
175196

176197
// Download video
177198
const { buffer } = this.muxer.target;
178-
this.downloadBlob(new Blob([buffer], { type: 'video/mp4' }));
199+
this.app.fire('encode:end', buffer);
179200

180201
// Free resources
181202
this.encoder.close();
182203
this.encoder = null;
183204
this.muxer = null;
184205

185-
console.log(`Recording stopped. Captured ${this.framesGenerated} frames.`);
186-
}
187-
188-
/**
189-
* Download the recorded video.
190-
*
191-
* @param {Blob} blob - The recorded video blob.
192-
* @private
193-
*/
194-
downloadBlob(blob) {
195-
const url = URL.createObjectURL(blob);
196-
197-
const a = document.createElement('a');
198-
a.href = url;
199-
a.download = 'recording.mp4';
200-
a.click();
206+
// Restore auto render state
207+
this.app.autoRender = originalAutoRender;
201208

202-
URL.revokeObjectURL(url);
209+
console.log(`Recording stopped. Captured ${this.totalFrames} frames.`);
203210
}
204211
}

0 commit comments

Comments
 (0)