oni/browser/src/Services/Recorder.ts

175 lines
5.6 KiB
TypeScript

/**
* Recorder.ts
*
* Manages a variety of recording scenarios, including:
* - Take & save screenshot
* - Record & save video (.webm)
*/
import { clipboard, desktopCapturer } from "electron"
import * as fs from "fs"
import * as path from "path"
import * as Oni from "oni-api"
import * as Log from "oni-core-logging"
import { configuration } from "./Configuration"
import { getInstance as getNotificationsInstance } from "./Notifications"
declare var MediaRecorder: any
const ONI_RECORDER_TITLE = "oni_recorder_title"
const toBuffer = (ab: ArrayBuffer) => {
const buffer = new Buffer(ab.byteLength)
const arr = new Uint8Array(ab)
for (let i = 0; i < arr.byteLength; i++) {
buffer[i] = arr[i]
}
return buffer
}
class Recorder implements Oni.Recorder {
private _recorder: any = null
private _blobs: Blob[] = []
public startRecording(): void {
const title = document.title
document.title = ONI_RECORDER_TITLE
desktopCapturer.getSources({ types: ["window", "screen"] }, (error, sources) => {
if (error) {
throw error
}
// tslint:disable-next-line prefer-for-of
for (let i = 0; i < sources.length; i++) {
const src = sources[i]
if (src.name === ONI_RECORDER_TITLE) {
document.title = title
const size = getDimensions()
// tslint:disable-next-line no-string-literal
navigator["webkitGetUserMedia"](
{
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: src.id,
minWidth: 320,
maxWidth: size.width,
minHeight: 240,
maxHeight: size.height,
},
},
},
(stream: any) => {
this._handleStream(stream)
},
(err: Error) => {
this._handleUserMediaError(err)
},
)
return
}
}
})
}
public get isRecording(): boolean {
return !!this._recorder
}
public async stopRecording(fileName?: string): Promise<void> {
this._recorder.stop()
const arrayBuffer = await toArrayBuffer(new Blob(this._blobs, { type: "video/webm" }))
const buffer = toBuffer(arrayBuffer)
fileName = fileName || getFileName("oni-video", "webm")
const videoFilePath = getOutputPath(fileName)
// TODO: Finish making this async
if (fs.existsSync(videoFilePath)) {
fs.unlinkSync(videoFilePath)
}
fs.writeFileSync(videoFilePath, buffer)
this._recorder = null
this._blobs = []
const notification = getNotificationsInstance().createItem()
notification.setContents("Recording finished", "Recording saved to: " + videoFilePath)
notification.setLevel("success")
notification.show()
}
public takeScreenshot(fileName?: string, scale: number = 1): void {
const webContents = require("electron").remote.getCurrentWebContents()
webContents.capturePage(image => {
const pngBuffer = image.toPNG({ scaleFactor: scale })
fileName = fileName || getFileName("oni-screenshot", "png")
const screenshotPath = getOutputPath(fileName)
fs.writeFileSync(screenshotPath, pngBuffer)
if (configuration.getValue("recorder.copyScreenshotToClipboard")) {
clipboard.writeImage(screenshotPath as any)
}
const notification = getNotificationsInstance().createItem()
notification.setContents("Screenshot taken", "Screenshot saved to " + screenshotPath)
notification.setLevel("success")
notification.show()
})
}
private _handleStream(stream: any) {
this._recorder = new MediaRecorder(stream)
this._blobs = []
this._recorder.ondataavailable = (evt: any) => {
this._blobs.push(evt.data)
}
this._recorder.start(100 /* ms */)
}
private _handleUserMediaError(err: Error) {
Log.error(err)
}
}
// Some of this code was adapted and modified from this stackoverflow post:
// https://stackoverflow.com/questions/36753288/saving-desktopcapturer-to-video-file-in-electron
const toArrayBuffer = async (blob: Blob): Promise<ArrayBuffer> => {
return new Promise<ArrayBuffer>((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = evt => {
const arrayBuffer = fileReader.result as ArrayBuffer
resolve(arrayBuffer)
}
fileReader.readAsArrayBuffer(blob)
})
}
const getDimensions = () => {
const size = require("electron")
.remote.getCurrentWindow()
.getSize()
return {
width: size[0],
height: size[1],
}
}
const getFileName = (fileBase: string, fileExtension: string) => {
return `${fileBase}-${new Date().getTime()}.${fileExtension}`
}
const getOutputPath = (fileName: string) => {
const outputPath = configuration.getValue("recorder.outputPath")
return path.join(outputPath, fileName)
}
export const recorder = new Recorder()