webrtc: convert RTCSignalingChannel to FFmpegInput + consolidate all converters (#1558)

* webrtc: add converter from RTCSignalingChannel to FFmpegInput

* consolidate buffer converters to WebRTCBridge as MediaConverter

* consolidate all converters into base plugin device

* remove unused
This commit is contained in:
Brett Jia
2024-08-17 13:31:15 -04:00
committed by GitHub
parent 17900f0589
commit a55099de12
4 changed files with 104 additions and 79 deletions

View File

@@ -16,8 +16,6 @@ import { RtpCodecCopy, RtpTrack, RtpTracks, startRtpForwarderProcess } from "./r
import { getAudioCodec, getFFmpegRtpAudioOutputArguments } from "./webrtc-required-codecs";
import { WeriftSignalingSession } from "./werift-signaling-session";
export const RTC_BRIDGE_NATIVE_ID = 'rtc-bridge';
function getDebugModeH264EncoderArgs() {
return [
'-profile:v', 'baseline',

View File

@@ -6,14 +6,14 @@ import { createBrowserSignalingSession } from "@scrypted/common/src/rtc-connect"
import { legacyGetSignalingSessionOptions } from '@scrypted/common/src/rtc-signaling';
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from '@scrypted/common/src/settings-mixin';
import { createZygote } from '@scrypted/common/src/zygote';
import sdk, { BufferConverter, ConnectOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MediaObjectOptions, MixinProvider, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingOptions, RTCSignalingSession, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from '@scrypted/sdk';
import sdk, { BufferConverter, MediaConverter, ConnectOptions, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, HttpRequest, Intercom, MediaObject, MediaObjectOptions, MixinProvider, RTCSessionControl, RTCSignalingChannel, RTCSignalingClient, RTCSignalingOptions, RTCSignalingSession, RequestMediaStream, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import ip from 'ip';
import net from 'net';
import os from 'os';
import { DataChannelDebouncer } from './datachannel-debouncer';
import { RTC_BRIDGE_NATIVE_ID, WebRTCConnectionManagement, createRTCPeerConnectionSink, createTrackForwarder } from "./ffmpeg-to-wrtc";
import { WebRTCConnectionManagement, createRTCPeerConnectionSink, createTrackForwarder } from "./ffmpeg-to-wrtc";
import { stunServers, turnServers, weriftStunServers, weriftTurnServers } from './ice-servers';
import { waitClosed } from './peerconnection-util';
import { WebRTCCamera } from "./webrtc-camera";
@@ -192,7 +192,7 @@ class WebRTCMixin extends SettingsMixinDeviceBase<RTCSignalingClient & VideoCame
}
}
export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreator, DeviceProvider, BufferConverter, MixinProvider, Settings {
export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreator, DeviceProvider, MediaConverter, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
iceInterfaceAddresses: {
title: 'ICE Interface Addresses',
@@ -256,25 +256,19 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
multiple: true,
}
});
bridge: WebRTCBridge;
activeConnections = 0;
constructor() {
super();
this.unshiftMixin = true;
this.fromMimeType = '*/*';
this.toMimeType = ScryptedMimeTypes.RTCSignalingChannel;
this.converters = [
["*/*", ScryptedMimeTypes.RTCSignalingChannel],
[ScryptedMimeTypes.RTCSignalingSession, ScryptedMimeTypes.RTCConnectionManagement],
[ScryptedMimeTypes.RTCSignalingChannel, ScryptedMimeTypes.FFmpegInput],
]
deviceManager.onDeviceDiscovered({
name: 'RTC Connection Bridge',
type: ScryptedDeviceType.API,
nativeId: RTC_BRIDGE_NATIVE_ID,
interfaces: [
ScryptedInterface.BufferConverter,
],
})
.then(() => this.bridge = new WebRTCBridge(this, RTC_BRIDGE_NATIVE_ID));
deviceManager.onDevicesChanged({ devices: [] });
}
getSettings(): Promise<Setting[]> {
@@ -285,7 +279,7 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
return this.storageSettings.putSetting(key, value);
}
async convert(data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<RTCSignalingChannel> {
async convertToSignalingChannel(data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<RTCSignalingChannel> {
const plugin = this;
const console = deviceManager.getMixinConsole(options?.sourceId, this.nativeId);
@@ -330,6 +324,98 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
}
}
async convertToRTCConnectionManagement(result: ReturnType<typeof zygote>, cleanup: Deferred<string>, data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<any> {
const weriftConfiguration = await this.getWeriftConfiguration();
const session = data as RTCSignalingSession;
const maximumCompatibilityMode = !!this.storageSettings.values.maximumCompatibilityMode;
const clientOptions = await legacyGetSignalingSessionOptions(session);
let connection: WebRTCConnectionManagement;
try {
const { createConnection } = await result.result;
connection = await createConnection({}, undefined, session,
maximumCompatibilityMode,
clientOptions,
{
configuration: this.getRTCConfiguration(),
weriftConfiguration,
ipv4Ban: this.storageSettings.values.ipv4Ban,
}
);
}
catch (e) {
result.worker.terminate();
throw e;
}
handleCleanupConnection(cleanup, connection, result);
await connection.negotiateRTCSignalingSession();
await connection.waitConnected();
return connection;
}
async convertToFFmpegInput(result: ReturnType<typeof zygote>, cleanup: Deferred<string>, data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<any> {
const channel = data as RTCSignalingChannel;
try {
const { createRTCPeerConnectionSource } = await result.result;
const rtcSource = await createRTCPeerConnectionSource({
__json_copy_serialize_children: true,
nativeId: undefined,
mixinId: undefined,
mediaStreamOptions: {
id: 'webrtc',
name: 'WebRTC',
source: 'cloud',
},
startRTCSignalingSession: (session) => channel.startRTCSignalingSession(session),
maximumCompatibilityMode: this.storageSettings.values.maximumCompatibilityMode,
});
const mediaStreamUrl = rtcSource.mediaObject;
return await mediaManager.convertMediaObject(mediaStreamUrl, ScryptedMimeTypes.FFmpegInput);
} catch (e) {
result.worker.terminate();
throw e;
}
}
async convertMedia(data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<any> {
const getFork = (cleanup: Deferred<string>) => {
const result = zygote();
this.activeConnections++;
result.worker.on('exit', () => {
this.activeConnections--;
cleanup.resolve('worker exited (convert)');
});
return result;
}
let converter: () => Promise<any>;
let cleanup = new Deferred<string>();
if (fromMimeType === ScryptedMimeTypes.RTCSignalingSession && toMimeType === ScryptedMimeTypes.RTCConnectionManagement) {
const result = getFork(cleanup);
converter = () => this.convertToRTCConnectionManagement(result, cleanup, data, fromMimeType, toMimeType, options);
}
else if (fromMimeType === ScryptedMimeTypes.RTCSignalingChannel && toMimeType === ScryptedMimeTypes.FFmpegInput) {
const result = getFork(cleanup);
converter = () => this.convertToFFmpegInput(result, cleanup, data, fromMimeType, toMimeType, options);
}
else if (toMimeType === ScryptedMimeTypes.RTCSignalingChannel) {
converter = () => this.convertToSignalingChannel(data, fromMimeType, toMimeType, options);
}
else {
throw new Error(`@scrypted/webrtc is unable to convert ${fromMimeType} to ${toMimeType}`);
}
try {
return await timeoutPromise(2 * 60 * 1000, converter());
}
catch (e) {
cleanup.resolve(e.toString());
throw e;
}
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
// if this is a webrtc camera, also proxy the signaling channel too
// for inflexible clients.
@@ -418,8 +504,6 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
}
async getDevice(nativeId: string) {
if (nativeId === RTC_BRIDGE_NATIVE_ID)
return this.bridge;
return new WebRTCCamera(this, nativeId);
}
@@ -715,61 +799,4 @@ export async function fork() {
}
}
class WebRTCBridge extends ScryptedDeviceBase implements BufferConverter {
constructor(public plugin: WebRTCPlugin, nativeId: string) {
super(nativeId);
this.fromMimeType = ScryptedMimeTypes.RTCSignalingSession;
this.toMimeType = ScryptedMimeTypes.RTCConnectionManagement;
}
async convertInternal(result: ReturnType<typeof zygote>, cleanup: Deferred<string>, data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<any> {
const weriftConfiguration = await this.plugin.getWeriftConfiguration();
const session = data as RTCSignalingSession;
const maximumCompatibilityMode = !!this.plugin.storageSettings.values.maximumCompatibilityMode;
const clientOptions = await legacyGetSignalingSessionOptions(session);
let connection: WebRTCConnectionManagement;
try {
const { createConnection } = await result.result;
connection = await createConnection({}, undefined, session,
maximumCompatibilityMode,
clientOptions,
{
configuration: this.plugin.getRTCConfiguration(),
weriftConfiguration,
ipv4Ban: this.plugin.storageSettings.values.ipv4Ban,
}
);
}
catch (e) {
result.worker.terminate();
throw e;
}
handleCleanupConnection(cleanup, connection, result);
await connection.negotiateRTCSignalingSession();
await connection.waitConnected();
return connection;
}
async convert(data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<any> {
const result = zygote();
this.plugin.activeConnections++;
const cleanup = new Deferred<string>();
result.worker.on('exit', () => {
this.plugin.activeConnections--;
cleanup.resolve('worker exited (convert)');
});
try {
return await timeoutPromise(2 * 60 * 1000, this.convertInternal(result, cleanup, data, fromMimeType, toMimeType, options));
}
catch (e) {
cleanup.resolve(e.toString());
throw e;
}
}
}
export default WebRTCPlugin;