@signalapp/ringrtc 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +41 -0
- package/dist/ringrtc/Service.d.ts +520 -0
- package/dist/ringrtc/Service.js +1462 -0
- package/dist/ringrtc/VideoSupport.d.ts +66 -0
- package/dist/ringrtc/VideoSupport.js +421 -0
- package/dist/test/RingRTC-test.d.ts +1 -0
- package/dist/test/RingRTC-test.js +71 -0
- package/package.json +55 -0
- package/scripts/fetch-prebuild.js +86 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
interface Ref<T> {
|
|
3
|
+
readonly current: T | null;
|
|
4
|
+
}
|
|
5
|
+
export declare enum VideoPixelFormatEnum {
|
|
6
|
+
I420 = 0,
|
|
7
|
+
Nv12 = 1,
|
|
8
|
+
Rgba = 2
|
|
9
|
+
}
|
|
10
|
+
export interface VideoFrameSource {
|
|
11
|
+
receiveVideoFrame(buffer: Buffer): [number, number] | undefined;
|
|
12
|
+
}
|
|
13
|
+
interface VideoFrameSender {
|
|
14
|
+
sendVideoFrame(width: number, height: number, format: VideoPixelFormatEnum, buffer: Buffer): void;
|
|
15
|
+
}
|
|
16
|
+
export declare class GumVideoCaptureOptions {
|
|
17
|
+
maxWidth: number;
|
|
18
|
+
maxHeight: number;
|
|
19
|
+
maxFramerate: number;
|
|
20
|
+
preferredDeviceId?: string;
|
|
21
|
+
screenShareSourceId?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class GumVideoCapturer {
|
|
24
|
+
private defaultCaptureOptions;
|
|
25
|
+
private localPreview?;
|
|
26
|
+
private captureOptions?;
|
|
27
|
+
private sender?;
|
|
28
|
+
private mediaStream?;
|
|
29
|
+
private spawnedSenderRunning;
|
|
30
|
+
private preferredDeviceId?;
|
|
31
|
+
private updateLocalPreviewIntervalId?;
|
|
32
|
+
constructor(defaultCaptureOptions: GumVideoCaptureOptions);
|
|
33
|
+
capturing(): boolean;
|
|
34
|
+
setLocalPreview(localPreview: Ref<HTMLVideoElement> | undefined): void;
|
|
35
|
+
enableCapture(): void;
|
|
36
|
+
enableCaptureAndSend(sender: VideoFrameSender, options?: GumVideoCaptureOptions): void;
|
|
37
|
+
disable(): void;
|
|
38
|
+
setPreferredDevice(deviceId: string): Promise<void>;
|
|
39
|
+
enumerateDevices(): Promise<MediaDeviceInfo[]>;
|
|
40
|
+
private getUserMedia;
|
|
41
|
+
private startCapturing;
|
|
42
|
+
private stopCapturing;
|
|
43
|
+
private startSending;
|
|
44
|
+
private spawnSender;
|
|
45
|
+
private stopSending;
|
|
46
|
+
private updateLocalPreviewSourceObject;
|
|
47
|
+
}
|
|
48
|
+
export declare const MAX_VIDEO_CAPTURE_WIDTH: number;
|
|
49
|
+
export declare const MAX_VIDEO_CAPTURE_HEIGHT: number;
|
|
50
|
+
export declare const MAX_VIDEO_CAPTURE_AREA: number;
|
|
51
|
+
export declare const MAX_VIDEO_CAPTURE_BUFFER_SIZE: number;
|
|
52
|
+
export declare class CanvasVideoRenderer {
|
|
53
|
+
private canvas?;
|
|
54
|
+
private buffer;
|
|
55
|
+
private imageData?;
|
|
56
|
+
private source?;
|
|
57
|
+
private rafId?;
|
|
58
|
+
constructor();
|
|
59
|
+
setCanvas(canvas: Ref<HTMLCanvasElement> | undefined): void;
|
|
60
|
+
enable(source: VideoFrameSource): void;
|
|
61
|
+
disable(): void;
|
|
62
|
+
private requestAnimationFrameCallback;
|
|
63
|
+
private renderBlack;
|
|
64
|
+
private renderVideoFrame;
|
|
65
|
+
}
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2019-2021 Signal Messenger, LLC
|
|
4
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
5
|
+
//
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.CanvasVideoRenderer = exports.MAX_VIDEO_CAPTURE_BUFFER_SIZE = exports.MAX_VIDEO_CAPTURE_AREA = exports.MAX_VIDEO_CAPTURE_HEIGHT = exports.MAX_VIDEO_CAPTURE_WIDTH = exports.GumVideoCapturer = exports.GumVideoCaptureOptions = exports.VideoPixelFormatEnum = void 0;
|
|
17
|
+
const index_1 = require("../index");
|
|
18
|
+
// Given a weird name to not conflict with WebCodec's VideoPixelFormat
|
|
19
|
+
var VideoPixelFormatEnum;
|
|
20
|
+
(function (VideoPixelFormatEnum) {
|
|
21
|
+
VideoPixelFormatEnum[VideoPixelFormatEnum["I420"] = 0] = "I420";
|
|
22
|
+
VideoPixelFormatEnum[VideoPixelFormatEnum["Nv12"] = 1] = "Nv12";
|
|
23
|
+
VideoPixelFormatEnum[VideoPixelFormatEnum["Rgba"] = 2] = "Rgba";
|
|
24
|
+
})(VideoPixelFormatEnum = exports.VideoPixelFormatEnum || (exports.VideoPixelFormatEnum = {}));
|
|
25
|
+
function videoPixelFormatFromEnum(format) {
|
|
26
|
+
switch (format) {
|
|
27
|
+
case VideoPixelFormatEnum.I420: {
|
|
28
|
+
return 'I420';
|
|
29
|
+
}
|
|
30
|
+
case VideoPixelFormatEnum.Nv12: {
|
|
31
|
+
return 'NV12';
|
|
32
|
+
}
|
|
33
|
+
case VideoPixelFormatEnum.Rgba: {
|
|
34
|
+
return 'RGBA';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function videoPixelFormatToEnum(format) {
|
|
39
|
+
switch (format) {
|
|
40
|
+
case 'I420': {
|
|
41
|
+
return VideoPixelFormatEnum.I420;
|
|
42
|
+
}
|
|
43
|
+
case 'NV12': {
|
|
44
|
+
return VideoPixelFormatEnum.Nv12;
|
|
45
|
+
}
|
|
46
|
+
case 'RGBA': {
|
|
47
|
+
return VideoPixelFormatEnum.Rgba;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
class GumVideoCaptureOptions {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.maxWidth = 640;
|
|
54
|
+
this.maxHeight = 480;
|
|
55
|
+
this.maxFramerate = 30;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.GumVideoCaptureOptions = GumVideoCaptureOptions;
|
|
59
|
+
class GumVideoCapturer {
|
|
60
|
+
constructor(defaultCaptureOptions) {
|
|
61
|
+
this.spawnedSenderRunning = false;
|
|
62
|
+
this.defaultCaptureOptions = defaultCaptureOptions;
|
|
63
|
+
}
|
|
64
|
+
capturing() {
|
|
65
|
+
return this.captureOptions != undefined;
|
|
66
|
+
}
|
|
67
|
+
setLocalPreview(localPreview) {
|
|
68
|
+
var _a;
|
|
69
|
+
const oldLocalPreview = (_a = this.localPreview) === null || _a === void 0 ? void 0 : _a.current;
|
|
70
|
+
if (oldLocalPreview) {
|
|
71
|
+
oldLocalPreview.srcObject = null;
|
|
72
|
+
}
|
|
73
|
+
this.localPreview = localPreview;
|
|
74
|
+
this.updateLocalPreviewSourceObject();
|
|
75
|
+
// This is a dumb hack around the fact that sometimes the
|
|
76
|
+
// this.localPreview.current is updated without a call
|
|
77
|
+
// to setLocalPreview, in which case the local preview
|
|
78
|
+
// won't be rendered.
|
|
79
|
+
if (this.updateLocalPreviewIntervalId != undefined) {
|
|
80
|
+
clearInterval(this.updateLocalPreviewIntervalId);
|
|
81
|
+
}
|
|
82
|
+
this.updateLocalPreviewIntervalId = setInterval(this.updateLocalPreviewSourceObject.bind(this), 1000);
|
|
83
|
+
}
|
|
84
|
+
enableCapture() {
|
|
85
|
+
// tslint:disable no-floating-promises
|
|
86
|
+
this.startCapturing(this.defaultCaptureOptions);
|
|
87
|
+
}
|
|
88
|
+
enableCaptureAndSend(sender, options) {
|
|
89
|
+
// tslint:disable no-floating-promises
|
|
90
|
+
this.startCapturing(options !== null && options !== void 0 ? options : this.defaultCaptureOptions);
|
|
91
|
+
this.startSending(sender);
|
|
92
|
+
}
|
|
93
|
+
disable() {
|
|
94
|
+
this.stopCapturing();
|
|
95
|
+
this.stopSending();
|
|
96
|
+
if (this.updateLocalPreviewIntervalId != undefined) {
|
|
97
|
+
clearInterval(this.updateLocalPreviewIntervalId);
|
|
98
|
+
}
|
|
99
|
+
this.updateLocalPreviewIntervalId = undefined;
|
|
100
|
+
}
|
|
101
|
+
setPreferredDevice(deviceId) {
|
|
102
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
103
|
+
this.preferredDeviceId = deviceId;
|
|
104
|
+
if (this.captureOptions) {
|
|
105
|
+
const captureOptions = this.captureOptions;
|
|
106
|
+
const sender = this.sender;
|
|
107
|
+
this.disable();
|
|
108
|
+
this.startCapturing(captureOptions);
|
|
109
|
+
if (sender) {
|
|
110
|
+
this.startSending(sender);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
enumerateDevices() {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
const devices = yield window.navigator.mediaDevices.enumerateDevices();
|
|
118
|
+
const cameras = devices.filter(d => d.kind == 'videoinput');
|
|
119
|
+
return cameras;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
getUserMedia(options) {
|
|
123
|
+
var _a;
|
|
124
|
+
// TODO: Figure out a better way to make typescript accept "mandatory".
|
|
125
|
+
let constraints = {
|
|
126
|
+
audio: false,
|
|
127
|
+
video: {
|
|
128
|
+
deviceId: (_a = options.preferredDeviceId) !== null && _a !== void 0 ? _a : this.preferredDeviceId,
|
|
129
|
+
width: {
|
|
130
|
+
max: options.maxWidth,
|
|
131
|
+
},
|
|
132
|
+
height: {
|
|
133
|
+
max: options.maxHeight,
|
|
134
|
+
},
|
|
135
|
+
frameRate: {
|
|
136
|
+
max: options.maxFramerate,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
if (options.screenShareSourceId != undefined) {
|
|
141
|
+
constraints.video = {
|
|
142
|
+
mandatory: {
|
|
143
|
+
chromeMediaSource: 'desktop',
|
|
144
|
+
chromeMediaSourceId: options.screenShareSourceId,
|
|
145
|
+
maxWidth: options.maxWidth,
|
|
146
|
+
maxHeight: options.maxHeight,
|
|
147
|
+
maxFrameRate: options.maxFramerate,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return window.navigator.mediaDevices.getUserMedia(constraints);
|
|
152
|
+
}
|
|
153
|
+
startCapturing(options) {
|
|
154
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
155
|
+
if (this.capturing()) {
|
|
156
|
+
index_1.RingRTC.logWarn('startCapturing(): already capturing');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
index_1.RingRTC.logInfo(`startCapturing(): ${options.maxWidth}x${options.maxHeight}@${options.maxFramerate}`);
|
|
160
|
+
this.captureOptions = options;
|
|
161
|
+
try {
|
|
162
|
+
// If we start/stop/start, we may have concurrent calls to getUserMedia,
|
|
163
|
+
// which is what we want if we're switching from camera to screenshare.
|
|
164
|
+
// But we need to make sure we deal with the fact that things might be
|
|
165
|
+
// different after the await here.
|
|
166
|
+
const mediaStream = yield this.getUserMedia(options);
|
|
167
|
+
// It's possible video was disabled, switched to screenshare, or
|
|
168
|
+
// switched to a different camera while awaiting a response, in
|
|
169
|
+
// which case we need to disable the camera we just accessed.
|
|
170
|
+
if (this.captureOptions != options) {
|
|
171
|
+
index_1.RingRTC.logWarn('startCapturing(): different state after getUserMedia()');
|
|
172
|
+
for (const track of mediaStream.getVideoTracks()) {
|
|
173
|
+
// Make the light turn off faster
|
|
174
|
+
track.stop();
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.mediaStream = mediaStream;
|
|
179
|
+
if (!this.spawnedSenderRunning &&
|
|
180
|
+
this.mediaStream != undefined &&
|
|
181
|
+
this.sender != undefined) {
|
|
182
|
+
this.spawnSender(this.mediaStream, this.sender);
|
|
183
|
+
}
|
|
184
|
+
this.updateLocalPreviewSourceObject();
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
index_1.RingRTC.logError(`startCapturing(): ${e}`);
|
|
188
|
+
// It's possible video was disabled, switched to screenshare, or
|
|
189
|
+
// switched to a different camera while awaiting a response, in
|
|
190
|
+
// which case we should reset the captureOptions if we set them.
|
|
191
|
+
if (this.captureOptions == options) {
|
|
192
|
+
// We couldn't open the camera. Oh well.
|
|
193
|
+
this.captureOptions = undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
stopCapturing() {
|
|
199
|
+
if (!this.capturing()) {
|
|
200
|
+
index_1.RingRTC.logWarn('stopCapturing(): not capturing');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
index_1.RingRTC.logInfo('stopCapturing()');
|
|
204
|
+
this.captureOptions = undefined;
|
|
205
|
+
if (!!this.mediaStream) {
|
|
206
|
+
for (const track of this.mediaStream.getVideoTracks()) {
|
|
207
|
+
// Make the light turn off faster
|
|
208
|
+
track.stop();
|
|
209
|
+
}
|
|
210
|
+
this.mediaStream = undefined;
|
|
211
|
+
}
|
|
212
|
+
this.updateLocalPreviewSourceObject();
|
|
213
|
+
}
|
|
214
|
+
startSending(sender) {
|
|
215
|
+
if (this.sender === sender) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!!this.sender) {
|
|
219
|
+
// If we're replacing an existing sender, make sure we stop the
|
|
220
|
+
// current setInterval loop before starting another one.
|
|
221
|
+
this.stopSending();
|
|
222
|
+
}
|
|
223
|
+
this.sender = sender;
|
|
224
|
+
if (!this.spawnedSenderRunning && this.mediaStream != undefined) {
|
|
225
|
+
this.spawnSender(this.mediaStream, this.sender);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
spawnSender(mediaStream, sender) {
|
|
229
|
+
const track = mediaStream.getVideoTracks()[0];
|
|
230
|
+
if (track == undefined || this.spawnedSenderRunning) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (track.readyState === "ended") {
|
|
234
|
+
this.stopCapturing();
|
|
235
|
+
index_1.RingRTC.logError(`spawnSender(): Video track ended before spawning sender`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const reader = new MediaStreamTrackProcessor({
|
|
239
|
+
track,
|
|
240
|
+
}).readable.getReader();
|
|
241
|
+
const buffer = Buffer.alloc(exports.MAX_VIDEO_CAPTURE_BUFFER_SIZE);
|
|
242
|
+
this.spawnedSenderRunning = true;
|
|
243
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
244
|
+
var _a;
|
|
245
|
+
try {
|
|
246
|
+
while (sender === this.sender && mediaStream == this.mediaStream) {
|
|
247
|
+
const { done, value: frame } = yield reader.read();
|
|
248
|
+
if (done) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (!frame) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const format = videoPixelFormatToEnum((_a = frame.format) !== null && _a !== void 0 ? _a : 'I420');
|
|
256
|
+
if (format == undefined) {
|
|
257
|
+
index_1.RingRTC.logWarn(`Unsupported video frame format: ${frame.format}`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
frame.copyTo(buffer);
|
|
261
|
+
sender.sendVideoFrame(frame.codedWidth, frame.codedHeight, format, buffer);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
index_1.RingRTC.logError(`sendVideoFrame(): ${e}`);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
// This must be called for more frames to come.
|
|
268
|
+
frame.close();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
index_1.RingRTC.logError(`spawnSender(): ${e}`);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
reader.releaseLock();
|
|
277
|
+
}
|
|
278
|
+
this.spawnedSenderRunning = false;
|
|
279
|
+
}))();
|
|
280
|
+
}
|
|
281
|
+
stopSending() {
|
|
282
|
+
// The spawned sender should stop
|
|
283
|
+
this.sender = undefined;
|
|
284
|
+
}
|
|
285
|
+
updateLocalPreviewSourceObject() {
|
|
286
|
+
if (!this.localPreview) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const localPreview = this.localPreview.current;
|
|
290
|
+
if (!localPreview) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const { mediaStream = null } = this;
|
|
294
|
+
if (localPreview.srcObject === mediaStream) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (mediaStream) {
|
|
298
|
+
localPreview.srcObject = mediaStream;
|
|
299
|
+
if (localPreview.width === 0) {
|
|
300
|
+
localPreview.width = this.captureOptions.maxWidth;
|
|
301
|
+
}
|
|
302
|
+
if (localPreview.height === 0) {
|
|
303
|
+
localPreview.height = this.captureOptions.maxHeight;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
localPreview.srcObject = null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
exports.GumVideoCapturer = GumVideoCapturer;
|
|
312
|
+
// We add 10% in each dimension to allow for things that are slightly wider or taller than 1080p.
|
|
313
|
+
const MAX_VIDEO_CAPTURE_MULTIPLIER = 1.0;
|
|
314
|
+
exports.MAX_VIDEO_CAPTURE_WIDTH = 1920 * MAX_VIDEO_CAPTURE_MULTIPLIER;
|
|
315
|
+
exports.MAX_VIDEO_CAPTURE_HEIGHT = 1080 * MAX_VIDEO_CAPTURE_MULTIPLIER;
|
|
316
|
+
exports.MAX_VIDEO_CAPTURE_AREA = exports.MAX_VIDEO_CAPTURE_WIDTH * exports.MAX_VIDEO_CAPTURE_HEIGHT;
|
|
317
|
+
exports.MAX_VIDEO_CAPTURE_BUFFER_SIZE = exports.MAX_VIDEO_CAPTURE_AREA * 4;
|
|
318
|
+
class CanvasVideoRenderer {
|
|
319
|
+
constructor() {
|
|
320
|
+
this.buffer = Buffer.alloc(exports.MAX_VIDEO_CAPTURE_BUFFER_SIZE);
|
|
321
|
+
}
|
|
322
|
+
setCanvas(canvas) {
|
|
323
|
+
this.canvas = canvas;
|
|
324
|
+
}
|
|
325
|
+
enable(source) {
|
|
326
|
+
if (this.source === source) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!!this.source) {
|
|
330
|
+
// If we're replacing an existing source, make sure we stop the
|
|
331
|
+
// current rAF loop before starting another one.
|
|
332
|
+
if (this.rafId) {
|
|
333
|
+
window.cancelAnimationFrame(this.rafId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.source = source;
|
|
337
|
+
this.requestAnimationFrameCallback();
|
|
338
|
+
}
|
|
339
|
+
disable() {
|
|
340
|
+
this.renderBlack();
|
|
341
|
+
this.source = undefined;
|
|
342
|
+
if (this.rafId) {
|
|
343
|
+
window.cancelAnimationFrame(this.rafId);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
requestAnimationFrameCallback() {
|
|
347
|
+
this.renderVideoFrame();
|
|
348
|
+
this.rafId = window.requestAnimationFrame(this.requestAnimationFrameCallback.bind(this));
|
|
349
|
+
}
|
|
350
|
+
renderBlack() {
|
|
351
|
+
if (!this.canvas) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const canvas = this.canvas.current;
|
|
355
|
+
if (!canvas) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const context = canvas.getContext('2d');
|
|
359
|
+
if (!context) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
context.fillStyle = 'black';
|
|
363
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
364
|
+
}
|
|
365
|
+
renderVideoFrame() {
|
|
366
|
+
var _a, _b;
|
|
367
|
+
if (!this.source || !this.canvas) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const canvas = this.canvas.current;
|
|
371
|
+
if (!canvas) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const context = canvas.getContext('2d');
|
|
375
|
+
if (!context) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const frame = this.source.receiveVideoFrame(this.buffer);
|
|
379
|
+
if (!frame) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const [width, height] = frame;
|
|
383
|
+
if (canvas.clientWidth <= 0 ||
|
|
384
|
+
width <= 0 ||
|
|
385
|
+
canvas.clientHeight <= 0 ||
|
|
386
|
+
height <= 0) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const frameAspectRatio = width / height;
|
|
390
|
+
const canvasAspectRatio = canvas.clientWidth / canvas.clientHeight;
|
|
391
|
+
let dx = 0;
|
|
392
|
+
let dy = 0;
|
|
393
|
+
if (frameAspectRatio > canvasAspectRatio) {
|
|
394
|
+
// Frame wider than view: We need bars at the top and bottom
|
|
395
|
+
canvas.width = width;
|
|
396
|
+
canvas.height = width / canvasAspectRatio;
|
|
397
|
+
dy = (canvas.height - height) / 2;
|
|
398
|
+
}
|
|
399
|
+
else if (frameAspectRatio < canvasAspectRatio) {
|
|
400
|
+
// Frame narrower than view: We need pillars on the sides
|
|
401
|
+
canvas.width = height * canvasAspectRatio;
|
|
402
|
+
canvas.height = height;
|
|
403
|
+
dx = (canvas.width - width) / 2;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// Will stretch perfectly with no bars
|
|
407
|
+
canvas.width = width;
|
|
408
|
+
canvas.height = height;
|
|
409
|
+
}
|
|
410
|
+
if (dx > 0 || dy > 0) {
|
|
411
|
+
context.fillStyle = 'black';
|
|
412
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
413
|
+
}
|
|
414
|
+
if (((_a = this.imageData) === null || _a === void 0 ? void 0 : _a.width) !== width || ((_b = this.imageData) === null || _b === void 0 ? void 0 : _b.height) !== height) {
|
|
415
|
+
this.imageData = new ImageData(width, height);
|
|
416
|
+
}
|
|
417
|
+
this.imageData.data.set(this.buffer.subarray(0, width * height * 4));
|
|
418
|
+
context.putImageData(this.imageData, dx, dy);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
exports.CanvasVideoRenderer = CanvasVideoRenderer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2019-2021 Signal Messenger, LLC
|
|
4
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
5
|
+
//
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const chaiAsPromised = require("chai-as-promised");
|
|
18
|
+
const index_1 = require("../index");
|
|
19
|
+
(0, chai_1.use)(chaiAsPromised);
|
|
20
|
+
describe('RingRTC', () => {
|
|
21
|
+
it('testsInitialization', () => {
|
|
22
|
+
chai_1.assert.isNotNull(index_1.RingRTC, "RingRTC didn't initialize!");
|
|
23
|
+
});
|
|
24
|
+
it('reports an age for expired offers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
|
+
const offer = {
|
|
26
|
+
offer: {
|
|
27
|
+
callId: { high: 0, low: 123 },
|
|
28
|
+
type: index_1.OfferType.AudioCall,
|
|
29
|
+
opaque: Buffer.from([]),
|
|
30
|
+
},
|
|
31
|
+
supportsMultiRing: true,
|
|
32
|
+
};
|
|
33
|
+
const age = 60 * 60;
|
|
34
|
+
try {
|
|
35
|
+
const { reason, ageSec: reportedAge } = yield new Promise((resolve, _reject) => {
|
|
36
|
+
index_1.RingRTC.handleAutoEndedIncomingCallRequest = (_callId, _remoteUserId, reason, ageSec) => {
|
|
37
|
+
resolve({ reason, ageSec });
|
|
38
|
+
};
|
|
39
|
+
index_1.RingRTC.handleCallingMessage('remote', null, 4, 2, age, 1, offer, Buffer.from([]), Buffer.from([]));
|
|
40
|
+
});
|
|
41
|
+
chai_1.assert.equal(reason, index_1.CallEndedReason.ReceivedOfferExpired);
|
|
42
|
+
chai_1.assert.equal(reportedAge, age);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
index_1.RingRTC.handleAutoEndedIncomingCallRequest = null;
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
it('reports 0 as the age of other auto-ended offers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
const offer = {
|
|
50
|
+
offer: {
|
|
51
|
+
callId: { high: 0, low: 123 },
|
|
52
|
+
type: index_1.OfferType.AudioCall,
|
|
53
|
+
opaque: Buffer.from([]),
|
|
54
|
+
},
|
|
55
|
+
supportsMultiRing: true,
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const { reason, ageSec: reportedAge } = yield new Promise((resolve, _reject) => {
|
|
59
|
+
index_1.RingRTC.handleAutoEndedIncomingCallRequest = (_callId, _remoteUserId, reason, ageSec) => {
|
|
60
|
+
resolve({ reason, ageSec });
|
|
61
|
+
};
|
|
62
|
+
index_1.RingRTC.handleCallingMessage('remote', null, 4, 2, 10, 2, offer, Buffer.from([]), Buffer.from([]));
|
|
63
|
+
});
|
|
64
|
+
chai_1.assert.equal(reason, index_1.CallEndedReason.Declined); // because we didn't set handleIncomingCall.
|
|
65
|
+
chai_1.assert.equal(reportedAge, 0);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
index_1.RingRTC.handleAutoEndedIncomingCallRequest = null;
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@signalapp/ringrtc",
|
|
3
|
+
"version": "2.23.0",
|
|
4
|
+
"description": "Signal Messenger voice and video calling library.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/*",
|
|
9
|
+
"scripts/fetch-prebuild.js"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"clean": "rimraf dist",
|
|
14
|
+
"test": "electron-mocha --recursive dist/test",
|
|
15
|
+
"eslint": "eslint --cache .",
|
|
16
|
+
"lint": "yarn format --list-different && yarn eslint",
|
|
17
|
+
"format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"",
|
|
18
|
+
"install": "node scripts/fetch-prebuild.js",
|
|
19
|
+
"prepublishOnly": "node scripts/prepublish.js"
|
|
20
|
+
},
|
|
21
|
+
"config": {
|
|
22
|
+
"prebuildUrl": "https://build-artifacts.signal.org/libraries/ringrtc-desktop-build-v${npm_package_version}.tar.gz",
|
|
23
|
+
"prebuildChecksum": "92f2a82a2acc5b853855a17b57f31354166783ec938f2452563f07e32e263c78"
|
|
24
|
+
},
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "AGPL-3.0-only",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"tar": "^6.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/chai": "4.2.18",
|
|
32
|
+
"@types/chai-as-promised": "^7.1.3",
|
|
33
|
+
"@types/dom-mediacapture-transform": "0.1.2",
|
|
34
|
+
"@types/mocha": "5.2.7",
|
|
35
|
+
"@types/node": "14.14.37",
|
|
36
|
+
"@types/offscreencanvas": "^2019.6.4",
|
|
37
|
+
"@types/react": "16.8.5",
|
|
38
|
+
"chai": "4.3.5",
|
|
39
|
+
"chai-as-promised": "^7.1.1",
|
|
40
|
+
"electron": "15.3.2",
|
|
41
|
+
"electron-mocha": "11.0.2",
|
|
42
|
+
"eslint": "7.32.0",
|
|
43
|
+
"eslint-config-airbnb-typescript-prettier": "4.2.0",
|
|
44
|
+
"eslint-config-prettier": "6.15.0",
|
|
45
|
+
"eslint-plugin-import": "2.25.4",
|
|
46
|
+
"eslint-plugin-mocha": "9.0.0",
|
|
47
|
+
"eslint-plugin-more": "1.0.5",
|
|
48
|
+
"eslint-plugin-react": "7.28.0",
|
|
49
|
+
"mocha": "9.2.0",
|
|
50
|
+
"prettier": "^2.5.1",
|
|
51
|
+
"rimraf": "3.0.2",
|
|
52
|
+
"typescript": "4.5.2",
|
|
53
|
+
"yarn-audit-fix": "^9.2.2"
|
|
54
|
+
}
|
|
55
|
+
}
|