Skip to content

Instantly share code, notes, and snippets.

@cletusw
Created April 24, 2021 17:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cletusw/f3a2d1aab7232696509f1aa4b107af78 to your computer and use it in GitHub Desktop.
Save cletusw/f3a2d1aab7232696509f1aa4b107af78 to your computer and use it in GitHub Desktop.
Video recorder custom element
import { html, css, LitElement } from 'lit';
import { ref, createRef } from 'lit/directives/ref.js';
export class VideoRecorder extends LitElement {
static get styles() {
return css`
video {
background: #222;
--width: 100%;
width: var(--width);
max-width: 1280px;
height: calc(var(--width) * 0.5625);
max-height: 720px;
}
`;
}
static get properties() {
return {
_errorMsg: { state: true },
_state: { state: true },
};
}
constructor() {
super();
this._errorMsg = '';
this._mediaRecorder = null;
this._recordedBlobs = null;
this._recordedVideoRef = createRef();
// empty, preview, recording, recorded
this._state = 'empty';
this._stream = null;
}
reset() {
this._errorMsg = '';
this._mediaRecorder = null;
this._recordedBlobs = null;
this._recordedVideoRef.value.src = null;
this._recordedVideoRef.value.srcObject = null;
this._state = 'empty';
this.stopCamera();
}
render() {
return html`
<!-- Based on https://github.com/webrtc/samples/blob/gh-pages/src/content/getusermedia/record -->
<video
playsinline
autoplay
muted
?hidden=${this._state === 'recorded'}
.srcObject=${this._stream}
></video>
<video
playsinline
controls
loop
?hidden=${this._state !== 'recorded'}
${ref(this._recordedVideoRef)}
></video>
<div>
<button @click=${this.reset}>
Reset
</button>
<button
@click=${this.startCamera}
?hidden=${this._state !== 'empty' && this._state !== 'recorded'}
>
Start camera
</button>
<button @click=${this.cancel} ?hidden=${this._state !== 'preview'}>
Cancel
</button>
<button
@click=${this.startRecording}
?disabled=${this._state !== 'preview'}
?hidden=${this._state === 'recording'}
>
Start recording
</button>
<button
@click=${this.stopRecording}
?hidden=${this._state !== 'recording'}
>
Stop recording
</button>
</div>
<div>
<span id="errorMsg">
${this._errorMsg}
</span>
</div>
`;
}
async startCamera() {
if (
!this._state === 'empty' ||
!this._state === 'recorded' ||
this._stream
) {
throw new Error('Invalid state');
}
const constraints = {
audio: true,
video: {
width: 1280,
height: 720,
},
};
console.log('Using media constraints:', constraints);
await this.init(constraints);
}
stopCamera() {
this._stream?.getTracks().forEach(function (track) {
track.stop();
});
this._stream = null;
}
cancel() {
this.stopCamera();
this._state = 'empty'; // TODO: or 'recorded'
}
async init(constraints) {
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.handleSuccess(stream);
} catch (e) {
console.error('navigator.getUserMedia error:', e);
this._errorMsg = `navigator.getUserMedia error: ${e.toString()}`;
}
}
handleSuccess(stream) {
console.log('getUserMedia() got stream:', stream);
this._stream = stream;
this._state = 'preview';
}
startRecording() {
this._recordedBlobs = [];
let options = { mimeType: 'video/webm;codecs=vp9,opus' };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: 'video/webm;codecs=vp8,opus' };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: 'video/webm' };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
options = { mimeType: '' };
}
}
}
try {
this._mediaRecorder = new MediaRecorder(this._stream, options);
} catch (e) {
console.error('Exception while creating MediaRecorder:', e);
this._errorMsg = `Exception while creating MediaRecorder: ${JSON.stringify(
e
)}`;
return;
}
console.log(
'Created MediaRecorder',
this._mediaRecorder,
'with options',
options
);
this._mediaRecorder.onstop = (event) => {
console.log('Recorder stopped: ', event);
console.log('Recorded Blobs: ', this._recordedBlobs);
};
this._mediaRecorder.ondataavailable = (event) => {
console.log('handleDataAvailable', event);
if (event.data && event.data.size > 0) {
this._recordedBlobs.push(event.data);
}
};
this._mediaRecorder.start();
this._state = 'recording';
console.log('MediaRecorder started', this._mediaRecorder);
}
stopRecording() {
if (!this._mediaRecorder) {
throw new Error('Invalid state');
}
this._mediaRecorder.stop();
this.stopCamera();
setTimeout(() => {
this.loadRecordedVideo();
this._state = 'recorded'; // TODO: check for valid data
}, 0);
}
loadRecordedVideo() {
console.log('Loading video', this._recordedVideoRef);
const superBuffer = new Blob(this._recordedBlobs, { type: 'video/webm' });
this._recordedVideoRef.value.src = null;
this._recordedVideoRef.value.srcObject = null;
this._recordedVideoRef.value.src = window.URL.createObjectURL(superBuffer);
this._recordedVideoRef.value.controls = true;
}
play() {
this._recordedVideoRef.value.play();
}
}
customElements.define('video-recorder', VideoRecorder);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment