mirror of
https://github.com/koush/scrypted.git
synced 2026-05-26 14:40:29 +01:00
various plugins: improve support for complex ffmpeg inputs
This commit is contained in:
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.246",
|
||||
"version": "0.0.247",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.246",
|
||||
"version": "0.0.247",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.246",
|
||||
"version": "0.0.247",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { createParserRebroadcaster, ParserSession, Rebroadcaster, startParserSession } from "@scrypted/common/src/ffmpeg-rebroadcast";
|
||||
import { createRawVideoParser, PIXEL_FORMAT_RGB24, StreamParser } from "@scrypted/common/src/stream-parser";
|
||||
import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, MediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, VideoCamera } from "@scrypted/sdk";
|
||||
import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, VideoCamera } from "@scrypted/sdk";
|
||||
const { systemManager, mediaManager } = sdk;
|
||||
|
||||
export interface AggregateDevice extends ScryptedDeviceBase {
|
||||
@@ -39,17 +37,7 @@ aggregators.set(ScryptedInterface.Lock,
|
||||
|
||||
|
||||
function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamera {
|
||||
let sessionPromise: Promise<{
|
||||
session: ParserSession<"rawvideo">;
|
||||
rebroadcaster: Rebroadcaster;
|
||||
parsers: { rawvideo: StreamParser };
|
||||
}>;
|
||||
|
||||
async function getVideoStreamWrapped(options: MediaStreamOptions) {
|
||||
if (sessionPromise) {
|
||||
console.error('session already active?');
|
||||
}
|
||||
|
||||
async function getVideoStreamWrapped(options: RequestMediaStreamOptions) {
|
||||
const args = await Promise.allSettled(devices.map(async (device) => {
|
||||
const mo = await device.getVideoStream();
|
||||
const buffer = await mediaManager.convertMediaObjectToBuffer(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
@@ -76,6 +64,8 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
|
||||
const filteredInput: FFmpegInput = {
|
||||
url: undefined,
|
||||
container: 'rawvideo',
|
||||
mediaStreamOptions: (await createVideoStreamOptions())?.[0],
|
||||
inputArguments: [],
|
||||
};
|
||||
|
||||
@@ -113,33 +103,7 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
filter.join(' '),
|
||||
);
|
||||
|
||||
const parsers = {
|
||||
rawvideo: createRawVideoParser({
|
||||
size: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
pixelFormat: PIXEL_FORMAT_RGB24,
|
||||
}),
|
||||
};
|
||||
const ret = await startParserSession(filteredInput, {
|
||||
console,
|
||||
parsers,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let rebroadcaster = await createParserRebroadcaster(ret, 'rawvideo', {
|
||||
idle: {
|
||||
timeout: 30000,
|
||||
callback: () => ret.kill(),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
session: ret,
|
||||
rebroadcaster,
|
||||
parsers,
|
||||
};
|
||||
return filteredInput;
|
||||
};
|
||||
|
||||
const createVideoStreamOptions: () => Promise<ResponseMediaStreamOptions[]> = async () => {
|
||||
@@ -148,7 +112,7 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
return [{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
container: 'rawvideo',
|
||||
container: 'ffmpeg',
|
||||
video: {},
|
||||
audio: null,
|
||||
}]
|
||||
@@ -163,24 +127,7 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
if (devices.length === 1)
|
||||
return devices[0].getVideoStream(options);
|
||||
|
||||
if (!sessionPromise) {
|
||||
sessionPromise = getVideoStreamWrapped(options);
|
||||
const ret = await sessionPromise;
|
||||
ret.session.killed.finally(() => sessionPromise = undefined);
|
||||
}
|
||||
|
||||
const ret = await sessionPromise;
|
||||
const { url } = ret.rebroadcaster;
|
||||
const ffmpegInput: FFmpegInput = {
|
||||
url,
|
||||
mediaStreamOptions: (await createVideoStreamOptions())?.[0],
|
||||
container: 'rawvideo',
|
||||
inputArguments: [
|
||||
...(ret.parsers.rawvideo.inputArguments || []),
|
||||
'-f', ret.parsers.rawvideo.container,
|
||||
'-i', url,
|
||||
],
|
||||
}
|
||||
const ffmpegInput = await getVideoStreamWrapped(options);
|
||||
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput);
|
||||
}
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.1.268",
|
||||
"version": "0.1.269",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.1.268",
|
||||
"version": "0.1.269",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.1.268",
|
||||
"version": "0.1.269",
|
||||
"description": "Rebroadcast and Prebuffer for VideoCameras.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -108,6 +108,10 @@ class PrebufferSession {
|
||||
}
|
||||
}
|
||||
|
||||
get canPrebuffer() {
|
||||
return this.advertisedMediaStreamOptions.container !== 'rawvideo' && this.advertisedMediaStreamOptions.container !== 'ffmpeg';
|
||||
}
|
||||
|
||||
getLastH264Probe(): H264Probe {
|
||||
const str = this.storage.getItem(this.lastH264ProbeKey);
|
||||
if (!str) {
|
||||
@@ -1189,8 +1193,8 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
let ffmpegInput: FFmpegInput;
|
||||
if (session.advertisedMediaStreamOptions.container === 'rawvideo') {
|
||||
this.console.log('Source is rawvideo. Using a direct media stream.');
|
||||
if (!session.canPrebuffer) {
|
||||
this.console.log('Source container can not be prebuffered. Using a direct media stream.');
|
||||
session = undefined;
|
||||
}
|
||||
if (!session) {
|
||||
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.26",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.26",
|
||||
"dependencies": {
|
||||
"@koush/werift": "file:../../external/werift/packages/webrtc",
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.26",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
|
||||
@@ -223,14 +223,13 @@ export async function createRTCPeerConnectionSink(
|
||||
const transcode = willTranscode
|
||||
|| mediaStreamOptions?.video?.codec !== 'h264'
|
||||
|| ffmpegInput.h264EncoderArguments?.length;
|
||||
|
||||
const width = Math.min(options?.screen?.width || 960, 1280);
|
||||
|
||||
if (transcode) {
|
||||
const conservativeDefaultBitrate = 500000;
|
||||
const bitrate = maximumCompatibilityMode ? conservativeDefaultBitrate : (ffmpegInput.destinationVideoBitrate || conservativeDefaultBitrate);
|
||||
const width = Math.min(options?.screen?.width || 960, 1280);
|
||||
videoArgs.push(
|
||||
// this might get wonky with 4:3?
|
||||
'-vf', `scale='min(${width},iw)':-2`,
|
||||
// this seems to cause issues with presets i think.
|
||||
// '-level:v', '4.0',
|
||||
"-b:v", bitrate.toString(),
|
||||
@@ -238,6 +237,16 @@ export async function createRTCPeerConnectionSink(
|
||||
"-maxrate", bitrate.toString(),
|
||||
'-r', '15',
|
||||
);
|
||||
|
||||
const scaleFilter = `scale='min(${width},iw)':-2`;
|
||||
let filterIndex = ffmpegInput.inputArguments.findIndex(f => f === '-vf');
|
||||
if (filterIndex === -1)
|
||||
filterIndex = ffmpegInput.inputArguments.findIndex(f => f === '-filter_complex');
|
||||
if (filterIndex !== -1)
|
||||
ffmpegInput.inputArguments[filterIndex + 1] = ffmpegInput.inputArguments[filterIndex + 1] + ` [unscaled]; [unscaled] ${scaleFilter}`;
|
||||
else
|
||||
videoArgs.push(scaleFilter)
|
||||
|
||||
if (!sessionSupportsH264High || maximumCompatibilityMode) {
|
||||
// baseline profile must use libx264, not sure other encoders properly support it.
|
||||
videoArgs.push(
|
||||
@@ -256,7 +265,7 @@ export async function createRTCPeerConnectionSink(
|
||||
}
|
||||
}
|
||||
else {
|
||||
videoArgs.push('-vcodec', 'copy')
|
||||
videoArgs.push('-vf', '-vcodec', 'copy')
|
||||
}
|
||||
|
||||
if (ffmpegInput.h264FilterArguments)
|
||||
|
||||
Reference in New Issue
Block a user