Created
April 24, 2021 17:02
-
-
Save cletusw/f3a2d1aab7232696509f1aa4b107af78 to your computer and use it in GitHub Desktop.
Video recorder custom element
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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