-
Notifications
You must be signed in to change notification settings - Fork 1
/
video.ts
150 lines (136 loc) · 4.75 KB
/
video.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { desktopCapturer } from "electron";
import { TaskEither, tryCatch } from "fp-ts/lib/TaskEither";
import { fromPromise, periodic, Stream } from "most";
import { create, insert } from "../../utils/lru";
import { writeBlobSafe, concatBlobsSafe, writeSafe } from "../../utils/blob";
import { logWith } from "../../utils/log";
import { sequenceTaskEitherArray } from "../../utils/task";
import { CommandStreams } from "../commands";
import { buildMergePartsCommand, execCommandIgnoreErrorSafe } from "../ffmpegCommands";
import { buildVideoPartPath, buildVideoPath } from "../pathBuilders";
const makeVideoConstraints = (id: string) => ({
audio: false,
video: {
mandatory: {
maxWidth: 1280,
maxHeight: 720,
maxFrameRate: 20,
chromeMediaSource: "desktop",
chromeMediaSourceId: id
}
}
});
export const getSourcesSafe = tryCatch<Error, Electron.DesktopCapturerSource[]>(
() =>
new Promise((res, rej) =>
desktopCapturer.getSources(
{ types: ["window", "screen"] },
(err, srcs) => (!!err ? rej(err) : res(srcs))
)
),
err => err as Error
);
export const getVideoMediaSafe = (sourceId: string) =>
tryCatch<Error, MediaStream>(
() => navigator.mediaDevices.getUserMedia(makeVideoConstraints(sourceId) as any),
err => err as Error
);
/**
* Create a MediaRecorder which emits a Blob after n seconds
*/
const createRecorderPromise = (stream: MediaStream): Promise<Blob> =>
new Promise(res => {
const recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9" });
recorder.ondataavailable = d => res(d.data);
setTimeout(() => {
recorder.stop();
}, 10000); //todo parameterize
recorder.start();
});
/**
* Create a new MediaRecorder every n second, outputing results via a single stream
*/
const createRecordingStream = (stream: MediaStream): Stream<Blob> =>
periodic(1000)
.take(1)
.chain(() => fromPromise(createRecorderPromise(stream)));
/**
* Setup via multiple MediaRecorder approach
* WARNING very resource intensive!
*/
export const multiRecorderSetup = (
cmds: CommandStreams,
ms: MediaStream
): Stream<TaskEither<Error, string>> =>
createRecordingStream(ms)
.sampleWith(cmds.captureStart$)
.map(writeBlobSafe(buildVideoPath()));
/**
* Setup via a single MediaRecorder, combining the blobs in memory
*/
export const singleRecorderInMemorySetup = (
cmds: CommandStreams,
ms: MediaStream
): Stream<TaskEither<Error, string>> =>
createMovingRecorder(cmds, ms)
.map(concatBlobsSafe)
.map(t => t.chain(writeSafe(buildVideoPath())));
/**
* Setup via a single MediaRecorder, persisting each blob then using FFMPEG to merge
*/
export const singleRecorderViaDiskSetup = (
cmds: CommandStreams,
ms: MediaStream
): Stream<TaskEither<Error, string>> =>
createMovingRecorder(cmds, ms)
.chain(addHeadBlob(ms))
.map(combineBlobParts);
/**
* Create a head blob and prepend it to the blobs from a single MediaStream
*/
const addHeadBlob = (ms: MediaStream) => (bs: Blob[]): Stream<Blob[]> =>
createHeadBlob(ms).map(headBlob => [headBlob, ...bs]);
/**
* Create a MediaRecorder for a single (tiny) blob. This blob will contain WebM metadata
*/
const createHeadBlob = (ms: MediaStream): Stream<Blob> =>
fromPromise(
new Promise(res => {
const rec = new MediaRecorder(ms, { mimeType: "video/webm;codecs=vp9" });
rec.ondataavailable = d => res(d.data);
setTimeout(() => {
rec.stop();
}, 150);
rec.start();
})
);
/**
* 15x1s blobs in an LRU cache via a single MediaRecorder
* Dumps data on captureStart$ request
*/
const createMovingRecorder = (cmds: CommandStreams, ms: MediaStream) => {
let blobs = create<Blob>(5, []);
const rec = new MediaRecorder(ms, { mimeType: "video/webm;codecs=vp9" });
rec.ondataavailable = d => {
blobs = insert(blobs, d.data);
};
rec.start();
//tried requestData and start(1000), no difference
setInterval(() => rec.requestData(), 1000);
return cmds.captureStart$.map(() => blobs.queue);
};
/**
* Write each blob to disk before using FFMPEG to merge them
*/
export const combineBlobParts = (bs: Blob[]): TaskEither<Error, string> => {
const fullPath = buildVideoPath();
const partPaths = sequenceTaskEitherArray(
bs.map((b, i) => writeBlobSafe(buildVideoPartPath(i))(b))
);
const fullClipPath = partPaths
.map(buildMergePartsCommand(fullPath))
.chain(execCommandIgnoreErrorSafe)
.map(logWith("Command ran:"))
.map(() => fullPath);
return fullClipPath;
};