diff --git a/common/src/ffmpeg-hardware-acceleration.ts b/common/src/ffmpeg-hardware-acceleration.ts index 7501ce608..cb403635f 100644 --- a/common/src/ffmpeg-hardware-acceleration.ts +++ b/common/src/ffmpeg-hardware-acceleration.ts @@ -87,11 +87,14 @@ export function getH264DecoderArgs(): CodecArgs { return ret; } +export const LIBX264_ENCODER_TITLE = 'libx264 (Software)'; + export function getH264EncoderArgs() { const encoders: { [type: string]: string } = {}; encoders['Copy Video, Transcode Audio'] = 'copy'; + if (isRaspberryPi()) { // encoders['Raspberry Pi OMX'] = 'h264_omx'; // encoders[V4L2] = 'h264_v4l2m2m'; @@ -128,5 +131,16 @@ export function getH264EncoderArgs() { '-pix_fmt', 'yuv420p', '-c:v', 'h264_v4l2m2m', ] } + + encoderArgs[LIBX264_ENCODER_TITLE] = getDebugModeH264EncoderArgs(); + return encoderArgs; } + +export function getDebugModeH264EncoderArgs() { + return [ + "-c:v", "libx264", + '-preset', 'ultrafast', + "-bf", "0", + ]; +} diff --git a/common/src/sdp-utils.ts b/common/src/sdp-utils.ts index 8fcba1ca6..457108176 100644 --- a/common/src/sdp-utils.ts +++ b/common/src/sdp-utils.ts @@ -177,6 +177,9 @@ export function parseMSection(msection: string[]) { else if (rtpmap?.includes('h264')) { codec = 'h264'; } + else if (rtpmap?.includes('h265')) { + codec = 'h265'; + } let direction: string; for (const checkDirection of ['sendonly' , 'sendrecv', 'recvonly' , 'inactive']) { diff --git a/plugins/homekit/src/camera-mixin.ts b/plugins/homekit/src/camera-mixin.ts index bc910a3f2..6d2b9d866 100644 --- a/plugins/homekit/src/camera-mixin.ts +++ b/plugins/homekit/src/camera-mixin.ts @@ -1,6 +1,5 @@ import sdk, { ObjectDetector, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, VideoCamera } from "@scrypted/sdk"; -import { getH264DecoderArgs, getH264EncoderArgs } from "../../../common/src/ffmpeg-hardware-acceleration"; -import { SettingsMixinDeviceOptions } from "../../../common/src/settings-mixin"; +import { SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin"; import { HomekitMixin } from "./homekit-mixin"; const { systemManager, deviceManager } = sdk; @@ -44,146 +43,35 @@ export class CameraMixin extends HomekitMixin { // type: 'boolean', // }); - settings.push({ - title: 'HomeKit Transcoding', - group: 'HomeKit Transcoding', - key: 'transcodingNotices', - value: 'WARNING', - readonly: true, - description: 'Transcoding audio and video for HomeKit is not recommended. Configure your camera using the camera web portal or app to output the correct HomeKit compatible codecs (h264/aac/2000kbps).', - }); - - settings.push({ - group: 'HomeKit Transcoding', - key: 'needsExtraData', - title: 'Add H264 Extra Data', - description: 'Some cameras do not include H264 extra data in the stream and this causes live streaming to always fail (but recordings may be working). This is a inexpensive video filter and does not perform a transcode. Enable this setting only as necessary.', - value: (this.storage.getItem('needsExtraData') === 'true').toString(), - type: 'boolean', - }); - - let showTranscodeArgs = this.storage.getItem('transcodeStreaming') === 'true'; - - if (hasMotionSensor) { - settings.push({ - title: 'Transcode Recording', - group: 'HomeKit Transcoding', - key: 'transcodeRecording', - type: 'boolean', - value: (this.storage.getItem('transcodeRecording') === 'true').toString(), - description: 'Use FFmpeg to transcode recordings to a format supported by HomeKit Secure Video.', - }); - - showTranscodeArgs = showTranscodeArgs || this.storage.getItem('transcodeRecording') === 'true'; - } - - settings.push({ - title: 'Transcode Streaming', - group: 'HomeKit Transcoding', - type: 'boolean', - key: 'transcodeStreaming', - value: (this.storage.getItem('transcodeStreaming') === 'true').toString(), - description: 'Use FFmpeg to transcode streaming to a format supported by HomeKit.', - }); - - const transcodeEnabledValues = [ - 'Transcode', - 'true', - ]; - - let hubStreamingMode = this.storage.getItem('hubStreamingMode'); - // 3/19/2022 migrate setting. - if (this.storage.getItem('transcodeStreamingHub') === 'true') { - if (!hubStreamingMode) - hubStreamingMode = 'Transcode'; - this.storage.removeItem('transcodeStreamingHub'); - } - - let watchStreamingMode = this.storage.getItem('watchStreamingMode'); - - showTranscodeArgs = showTranscodeArgs - || transcodeEnabledValues.includes(hubStreamingMode) - || transcodeEnabledValues.includes(watchStreamingMode); - if (this.interfaces.includes(ScryptedInterface.VideoCameraConfiguration)) { - const choices = [ - 'Disabled', - 'Transcode', - 'Adaptive Bitrate', - ]; - - if (hubStreamingMode === 'true') - hubStreamingMode = 'Transcode'; + let adaptiveBitrate: string[] = []; + try { + adaptiveBitrate = JSON.parse(this.storage.getItem('adaptiveBitrate')); + } + catch (e) { + } settings.push({ - title: 'Transcode Remote Streaming', - group: 'HomeKit Transcoding', - key: 'hubStreamingMode', - value: hubStreamingMode || 'Disabled', - choices, - description: 'The transcode options to use when remote streaming or streaming to limited capabilitity devices like Apple Watch. "Transcode" will use FFmpeg to stream a format supported by HomeKit. "Adaptive Bitrate" adjusts the bitrate of the native camera stream on demand to accomodate available bandwidth. Adaptive Bitrate should be used on secondary streams (sub streams), and not the main stream connected to an NVR, as it will reduce the recording quality.', - }); - - - if (watchStreamingMode === 'true') - watchStreamingMode = 'Transcode'; - - settings.push({ - title: 'Transcode Apple Watch Streaming', - group: 'HomeKit Transcoding', - key: 'watchStreamingMode', - value: watchStreamingMode || 'Disabled', - choices, - description: 'The transcode options to use when streaming to Apple Watch. "Transcode" will use FFmpeg to stream a format supported by HomeKit. "Adaptive Bitrate" adjusts the bitrate of the native camera stream on demand to accomodate available bandwidth. Adaptive Bitrate should be used on secondary streams (sub streams), and not the main stream connected to an NVR, as it will reduce the recording quality.', - }); - } - else { - - - settings.push({ - title: 'Transcode Remote Streaming', - group: 'HomeKit Transcoding', - type: 'boolean', - key: 'hubStreamingMode', - value: transcodeEnabledValues.includes(hubStreamingMode).toString(), - description: 'Transcode when remote streaming.', - }); - - settings.push({ - title: 'Transcode Apple Watch Streaming', - group: 'HomeKit Transcoding', - type: 'boolean', - key: 'watchStreamingMode', - value: transcodeEnabledValues.includes(this.storage.getItem('watchStreamingMode')).toString(), - description: 'Transcode when streaming to Apple Watch.', - }); + key: 'adaptiveBitrate', + title: 'Adaptive Bitrate Streaming', + description: 'Adaptive Bitrate adjusts the bitrate of the native camera stream on demand to accomodate available bandwidth. If the camera\'s primary stream is being recorded by an NVR, Adaptive Bitrate should be used on a secondary stream (sub stream), as it will reduce the recording quality.', + choices: [ + 'Local Stream', + 'Remote Stream', + 'Apple Watch', + ], + multiple: true, + value: adaptiveBitrate, + }) } - if (showTranscodeArgs) { - const decoderArgs = getH264DecoderArgs(); - const encoderArgs = getH264EncoderArgs(); - - settings.push({ - title: 'Video Decoder Arguments', - group: 'HomeKit Transcoding', - key: "videoDecoderArguments", - value: this.storage.getItem('videoDecoderArguments'), - description: 'FFmpeg arguments used to decode input video.', - placeholder: '-hwaccel auto', - choices: Object.keys(decoderArgs), - combobox: true, - }); - settings.push({ - title: 'H264 Encoder Arguments', - group: 'HomeKit Transcoding', - key: "h264EncoderArguments", - value: this.storage.getItem('h264EncoderArguments'), - description: 'FFmpeg arguments used to encode h264 video.', - placeholder: '-vcodec h264_omx', - choices: Object.keys(encoderArgs), - combobox: true, - }); - } + settings.push({ + title: 'Transcoding Debug Mode', + key: 'transcodingDebugMode', + description: 'Force transcoding on this camera for streaming and recording. This setting can be used to diagnose errors with HomeKit functionality. Enable the Rebroadcast plugin for more robust transcoding options.', + type: 'boolean', + value: (this.storage.getItem('transcodingDebugMode') === 'true').toString(), + }); if (this.interfaces.includes(ScryptedInterface.AudioSensor)) { settings.push({ @@ -249,28 +137,6 @@ export class CameraMixin extends HomekitMixin { return super.putMixinSetting(key, value); } - if (key === 'videoDecoderArguments') { - const decoderArgs = getH264DecoderArgs(); - value = decoderArgs[value.toString()]?.join(' ') || value; - } - - if (key === 'h264EncoderArguments') { - const encoderArgs = getH264EncoderArgs(); - const args = encoderArgs[value.toString()]; - if (args) { - // if default args were specified (ie, videotoolbox, quicksync, etc), - // expand that into args that include bitrate and rescale. - const extraEncoderArgs = [ - '-b:v', '${request.video.max_bit_rate * 2}k', - '-vf', 'scale=${request.video.width}:${request.video.height}', - '-r', '${request.video.fps}', - ]; - args.push(...extraEncoderArgs); - } - const substitute = args?.join(' '); - value = substitute ? `\`${substitute}\`` : value; - } - if (key === 'objectDetectionContactSensors') { this.storage.setItem(key, JSON.stringify(value)); } diff --git a/plugins/homekit/src/homekit-mixin.ts b/plugins/homekit/src/homekit-mixin.ts index e706965c6..ed590f01c 100644 --- a/plugins/homekit/src/homekit-mixin.ts +++ b/plugins/homekit/src/homekit-mixin.ts @@ -9,20 +9,21 @@ export const HOMEKIT_MIXIN = 'mixin:@scrypted/homekit'; export class HomekitMixin extends SettingsMixinDeviceBase { storageSettings = new StorageSettings(this, { resetAccessory: { - group: 'HomeKit Pairing', - title: 'Reset Accessory', - description: 'Bridged devices will automatically relink as a new device. Accessory devices must be manually removed from the Home app and re-paired.', - type: 'button', - // generate a new reset accessory random value. - onPut: () => { - // also reset the username/mac for standalone accessories - this.storage.removeItem('mac'); - this.alertReload(); + title: 'Reset Pairing', + description: 'Resetting the pairing will resync it to HomeKit as a new device. Bridged devices will automatically relink as a new device. Accessory devices must be manually removed from the Home app and re-paired. Enter RESET to reset the pairing.', + placeholder: 'RESET', + mapPut: (oldValue, newValue) => { + if (newValue === 'RESET') { + this.storage.removeItem('mac'); + this.alertReload(); + // generate a new reset accessory random value. + return crypto.randomBytes(8).toString('hex'); + } + throw new Error('HomeKit Accessory Reset cancelled.'); }, - mapPut: () => crypto.randomBytes(8).toString('hex'), + mapGet: () => '', }, standalone: { - group: 'HomeKit Pairing', title: 'Standalone Accessory', description: 'Experimental: Advertise this to HomeKit as a standalone accessory rather than through the Scrypted HomeKit bridge. Enabling this option will remove it from the bridge, and the accessory will then need to be re-paired to HomeKit.' + (this.interfaces.includes(ScryptedInterface.VideoCamera) diff --git a/plugins/homekit/src/types/camera/camera-recording.ts b/plugins/homekit/src/types/camera/camera-recording.ts index 42d9ce745..9da05d0da 100644 --- a/plugins/homekit/src/types/camera/camera-recording.ts +++ b/plugins/homekit/src/types/camera/camera-recording.ts @@ -1,4 +1,5 @@ +import { getDebugModeH264EncoderArgs } from "@scrypted/common/src/ffmpeg-hardware-acceleration"; import { FFmpegFragmentedMP4Session, parseFragmentedMP4, startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session'; import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers'; import sdk, { AudioSensor, FFmpegInput, MotionSensor, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, VideoCamera } from '@scrypted/sdk'; @@ -9,7 +10,7 @@ import { Duplex, Writable } from 'stream'; import { HomeKitSession } from '../../common'; import { AudioRecordingCodecType, AudioRecordingSamplerateValues, CameraRecordingConfiguration } from '../../hap'; import { getCameraRecordingFiles, HksvVideoClip, VIDEO_CLIPS_NATIVE_ID } from './camera-recording-files'; -import { evalRequest } from './camera-transcode'; +import { checkCompatibleCodec, transcodingDebugModeWarning } from './camera-utils'; const { log, mediaManager, deviceManager } = sdk; @@ -43,10 +44,15 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam const noAudio = ffmpegInput.mediaStreamOptions && ffmpegInput.mediaStreamOptions.audio === null; const audioCodec = ffmpegInput.mediaStreamOptions?.audio?.codec; + const videoCodec = ffmpegInput.mediaStreamOptions?.video?.codec; const isDefinitelyNotAAC = !audioCodec || audioCodec.toLowerCase().indexOf('aac') === -1; - const transcodeRecording = storage.getItem('transcodeRecording') === 'true'; + const transcodingDebugMode = storage.getItem('transcodingDebugMode') === 'true'; + const transcodeRecording = !!ffmpegInput.h264EncoderArguments?.length; const incompatibleStream = noAudio || transcodeRecording || isDefinitelyNotAAC; + if (transcodingDebugMode) + transcodingDebugModeWarning(); + let session: FFmpegFragmentedMP4Session & { socket?: Duplex }; if (ffmpegInput.container === 'mp4' && ffmpegInput.url.startsWith('tcp://') && !incompatibleStream) { @@ -70,12 +76,9 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam } } - if (transcodeRecording) { - // decoder arguments - const videoDecoderArguments = storage.getItem('videoDecoderArguments') || ''; - if (videoDecoderArguments) { - inputArguments.push(...evalRequest(videoDecoderArguments, request)); - } + // decoder arguments + if (transcodeRecording && ffmpegInput.videoDecoderArguments?.length) { + inputArguments.push(...ffmpegInput.videoDecoderArguments); } inputArguments.push(...ffmpegInput.inputArguments) @@ -89,8 +92,8 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam } let audioArgs: string[]; - if (noAudio || transcodeRecording || isDefinitelyNotAAC) { - if (!(noAudio || transcodeRecording)) + if (noAudio || transcodeRecording || isDefinitelyNotAAC || transcodingDebugMode) { + if (!(noAudio || transcodeRecording || transcodingDebugMode)) console.warn('Recording audio is not explicitly AAC, forcing transcoding. Setting audio output to AAC is recommended.', audioCodec); let aacLowEncoder = 'aac'; @@ -120,32 +123,24 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam } let videoArgs: string[]; - if (transcodeRecording) { - const h264EncoderArguments = storage.getItem('h264EncoderArguments') || ''; - videoArgs = h264EncoderArguments - ? evalRequest(h264EncoderArguments, request) : [ - "-vcodec", "libx264", - // '-preset', 'ultrafast', '-tune', 'zerolatency', - '-pix_fmt', 'yuvj420p', - // '-color_range', 'mpeg', - "-bf", "0", - // "-profile:v", profileToFfmpeg(request.video.profile), - // '-level:v', levelToFfmpeg(request.video.level), - '-b:v', `${configuration.videoCodec.bitrate}k`, - "-bufsize", (2 * request.video.max_bit_rate).toString() + "k", - "-maxrate", request.video.max_bit_rate.toString() + "k", - "-filter:v", `fps=${request.video.fps},scale=w=${configuration.videoCodec.resolution[0]}:h=${configuration.videoCodec.resolution[1]}:force_original_aspect_ratio=1,pad=${configuration.videoCodec.resolution[0]}:${configuration.videoCodec.resolution[1]}:(ow-iw)/2:(oh-ih)/2`, - '-force_key_frames', `expr:gte(t,n_forced*${iframeIntervalSeconds})`, - ]; + if (transcodingDebugMode) { + videoArgs = getDebugModeH264EncoderArgs(); + } + else if (transcodeRecording) { + videoArgs = [ + '-b:v', `${configuration.videoCodec.bitrate}k`, + "-bufsize", (2 * request.video.max_bit_rate).toString() + "k", + "-maxrate", request.video.max_bit_rate.toString() + "k", + "-filter:v", `fps=${request.video.fps},scale=w=${configuration.videoCodec.resolution[0]}:h=${configuration.videoCodec.resolution[1]}:force_original_aspect_ratio=1,pad=${configuration.videoCodec.resolution[0]}:${configuration.videoCodec.resolution[1]}:(ow-iw)/2:(oh-ih)/2`, + '-force_key_frames', `expr:gte(t,n_forced*${iframeIntervalSeconds})`, + + ...ffmpegInput.h264EncoderArguments, + ]; } else { + checkCompatibleCodec(console, device, videoCodec) videoArgs = [ '-vcodec', 'copy', - // Ran into an issue where the RTSP source had SPS/PPS in the SDP, - // and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames - // unless this flag is used. - // I believe this is causing issues. I think inly FTYP/MOOV should have SPS/PPS? - // "-bsf:v", "dump_extra", ]; } @@ -224,7 +219,6 @@ export async function* handleFragmentsRequests(device: ScryptedDevice & VideoCam saveFragment(i, fragment); pending = []; console.log(`motion fragment #${++i} sent. size:`, fragment.length); - // fs.writeFileSync(`/tmp/${device.id}-${i.toString().padStart(2, '0')}.mp4`, fragment); yield fragment; } } diff --git a/plugins/homekit/src/types/camera/camera-streaming-ffmpeg.ts b/plugins/homekit/src/types/camera/camera-streaming-ffmpeg.ts index 5bd051b6f..1ace41bf6 100644 --- a/plugins/homekit/src/types/camera/camera-streaming-ffmpeg.ts +++ b/plugins/homekit/src/types/camera/camera-streaming-ffmpeg.ts @@ -1,18 +1,18 @@ +import { getDebugModeH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration'; import { createBindZero } from '@scrypted/common/src/listen-cluster'; import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers'; -import sdk, { FFmpegInput, MediaStreamDestination, RequestMediaStreamOptions, ScryptedDevice, ScryptedMimeTypes, VideoCamera } from '@scrypted/sdk'; +import sdk, { FFmpegInput, ScryptedDevice, VideoCamera } from '@scrypted/sdk'; import child_process from 'child_process'; import { RtpPacket } from '../../../../../external/werift/packages/rtp/src/rtp/rtp'; -import { ProtectionProfileAes128CmHmacSha1_80 } from '../../../../../external/werift/packages/rtp/src/srtp/const'; import { AudioStreamingCodecType, SRTPCryptoSuites, StartStreamRequest } from '../../hap'; -import { evalRequest } from '../camera/camera-transcode'; import { CameraStreamingSession, KillCameraStreamingSession } from './camera-streaming-session'; import { startCameraStreamSrtp } from './camera-streaming-srtp'; import { createCameraStreamSender } from './camera-streaming-srtp-sender'; +import { checkCompatibleCodec, transcodingDebugModeWarning } from './camera-utils'; -const { mediaManager } = sdk; +const { mediaManager, log } = sdk; -export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCamera, console: Console, storage: Storage, videoInput: FFmpegInput, transcodeStreaming: boolean, session: CameraStreamingSession, killSession: KillCameraStreamingSession) { +export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCamera, console: Console, storage: Storage, ffmpegInput: FFmpegInput, session: CameraStreamingSession, killSession: KillCameraStreamingSession) { const request = session.startRequest; const videomtu = session.startRequest.video.mtu; @@ -30,17 +30,16 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame // any notable benefit with a prebuffer, which allows the ffmpeg analysis for key frame // to immediately finish. ffmpeg will only start sending on a key frame. // const audioInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(await device.getVideoStream(selectedStream), ScryptedMimeTypes.FFmpegInput)).toString()) as FFmpegInput; - const audioInput = videoInput; + const audioInput = ffmpegInput; const videoKey = Buffer.concat([session.prepareRequest.video.srtp_key, session.prepareRequest.video.srtp_salt]); const audioKey = Buffer.concat([session.prepareRequest.audio.srtp_key, session.prepareRequest.audio.srtp_salt]); - const mso = videoInput.mediaStreamOptions; + const mso = ffmpegInput.mediaStreamOptions; const noAudio = mso?.audio === null; const hideBanner = [ '-hide_banner', ]; - const decoderArgs: string[] = []; const videoArgs: string[] = []; const audioArgs: string[] = []; @@ -53,36 +52,42 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame nullAudioInput.push('-f', 'lavfi', '-i', 'anullsrc=cl=1', '-shortest'); } - // decoder args - if (transcodeStreaming) { - // decoder arguments - const videoDecoderArguments = storage.getItem('videoDecoderArguments') || ''; - if (videoDecoderArguments) { - decoderArgs.push(...evalRequest(videoDecoderArguments, request)); - } - } + const transcodingDebugMode = storage.getItem('transcodingDebugMode') === 'true'; + if (transcodingDebugMode) + transcodingDebugModeWarning(); + + const videoCodec = ffmpegInput.mediaStreamOptions?.video?.codec; + const needsFFmpeg = transcodingDebugMode || !!ffmpegInput.h264EncoderArguments?.length || !!ffmpegInput.h264FilterArguments?.length; videoArgs.push( "-an", '-sn', '-dn', ); // encoder args - if (transcodeStreaming) { - const h264EncoderArguments = storage.getItem('h264EncoderArguments') || ''; - const videoCodec = h264EncoderArguments - ? evalRequest(h264EncoderArguments, request) : + if (transcodingDebugMode) { + const videoCodec = [ - "-vcodec", "libx264", - // '-preset', 'ultrafast', '-tune', 'zerolatency', - '-pix_fmt', 'yuvj420p', - // '-color_range', 'mpeg', - "-bf", "0", - // "-profile:v", profileToFfmpeg(request.video.profile), - // '-level:v', levelToFfmpeg(request.video.level), "-b:v", request.video.max_bit_rate.toString() + "k", "-bufsize", (2 * request.video.max_bit_rate).toString() + "k", "-maxrate", request.video.max_bit_rate.toString() + "k", "-filter:v", "fps=" + request.video.fps.toString(), + + ...getDebugModeH264EncoderArgs(), + ]; + + videoArgs.push( + ...videoCodec, + ) + } + else if (ffmpegInput.h264EncoderArguments?.length) { + const videoCodec = + [ + "-b:v", request.video.max_bit_rate.toString() + "k", + "-bufsize", (2 * request.video.max_bit_rate).toString() + "k", + "-maxrate", request.video.max_bit_rate.toString() + "k", + "-filter:v", "fps=" + request.video.fps.toString(), + + ...ffmpegInput.h264EncoderArguments, ]; videoArgs.push( @@ -90,19 +95,15 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame ) } else { + checkCompatibleCodec(console, device, videoCodec) + videoArgs.push( "-vcodec", "copy", ); + } - // 3/6/2022 - // Ran into an issue where the RTSP source had SPS/PPS in the SDP, - // and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames - // unless this flag is used. - // 3/7/2022 - // This flag was enabled by default, but I believe this is causing issues with some users. - // Make it a setting. - if (storage.getItem('needsExtraData') === 'true') - videoArgs.push("-bsf:v", "dump_extra"); + if (ffmpegInput.h264FilterArguments?.length) { + videoArgs.push(...ffmpegInput.h264FilterArguments); } let videoOutput = `srtp://${session.prepareRequest.targetAddress}:${session.prepareRequest.video.port}?rtcpport=${session.prepareRequest.video.port}&pkt_size=${videomtu}`; @@ -137,7 +138,6 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame videoArgs.push( "-payload_type", (request as StartStreamRequest).video.pt.toString(), "-ssrc", session.videossrc.toString(), - // '-fflags', '+flush_packets', '-flush_packets', '1', "-f", "rtp", "-srtp_out_suite", session.prepareRequest.video.srtpCryptoSuite === SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 ? "AES_CM_128_HMAC_SHA1_80" : "AES_CM_256_HMAC_SHA1_80", @@ -166,37 +166,40 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame let hasAudio = true; - if (!transcodeStreaming + if (!needsFFmpeg && perfectOpus && mso?.tool === 'scrypted') { - await startCameraStreamSrtp(videoInput, console, session, killSession); + await startCameraStreamSrtp(ffmpegInput, console, session, killSession); return; - - audioArgs.push( - "-acodec", "copy", - ); } else if (audioCodec === AudioStreamingCodecType.OPUS || audioCodec === AudioStreamingCodecType.AAC_ELD) { - // by default opus encodes with a packet time of 20. however, homekit may request another value, - // which we will respect by simply outputing frames of that duration, rather than packing - // 20 ms frames to accomodate. - opusFramesPerPacket = 1; + if (!transcodingDebugMode && perfectOpus) { + audioArgs.push( + "-acodec", "copy", + ); + } + else { + // by default opus encodes with a packet time of 20. however, homekit may request another value, + // which we will respect by simply outputing frames of that duration, rather than packing + // 20 ms frames to accomodate. + opusFramesPerPacket = 1; - audioArgs.push( - '-acodec', ...(requestedOpus ? - [ - 'libopus', - '-application', 'lowdelay', - '-frame_duration', (request as StartStreamRequest).audio.packet_time.toString(), - ] : - ['libfdk_aac', '-profile:a', 'aac_eld']), - '-flags', '+global_header', - '-ar', `${(request as StartStreamRequest).audio.sample_rate}k`, - '-b:a', `${(request as StartStreamRequest).audio.max_bit_rate}k`, - "-bufsize", `${(request as StartStreamRequest).audio.max_bit_rate * 4}k`, - '-ac', `${(request as StartStreamRequest).audio.channel}`, - ) + audioArgs.push( + '-acodec', ...(requestedOpus ? + [ + 'libopus', + '-application', 'lowdelay', + '-frame_duration', (request as StartStreamRequest).audio.packet_time.toString(), + ] : + ['libfdk_aac', '-profile:a', 'aac_eld']), + '-flags', '+global_header', + '-ar', `${(request as StartStreamRequest).audio.sample_rate}k`, + '-b:a', `${(request as StartStreamRequest).audio.max_bit_rate}k`, + "-bufsize", `${(request as StartStreamRequest).audio.max_bit_rate * 4}k`, + '-ac', `${(request as StartStreamRequest).audio.channel}`, + ) + } } else { hasAudio = false; @@ -240,8 +243,6 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame ? "AES_CM_128_HMAC_SHA1_80" : "AES_CM_256_HMAC_SHA1_80", "-srtp_out_params", audioKey.toString('base64'), - // not sure this has any effect? testing. - // '-fflags', '+flush_packets', '-flush_packets', '1', `srtp://${session.prepareRequest.targetAddress}:${session.prepareRequest.audio.port}?rtcpport=${session.prepareRequest.audio.port}&pkt_size=${audiomtu}` ) } @@ -255,14 +256,16 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame return; } - if (audioInput !== videoInput) { + const videoDecoderArguments = ffmpegInput.videoDecoderArguments || []; + + if (audioInput !== ffmpegInput) { safePrintFFmpegArguments(console, videoArgs); safePrintFFmpegArguments(console, audioArgs); const vp = child_process.spawn(ffmpegPath, [ ...hideBanner, - ...decoderArgs, - ...videoInput.inputArguments, + ...videoDecoderArguments, + ...ffmpegInput.inputArguments, ...videoArgs, ]); session.videoProcess = vp; @@ -271,7 +274,6 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame const ap = child_process.spawn(ffmpegPath, [ ...hideBanner, - ...decoderArgs, ...audioInput.inputArguments, ...nullAudioInput, ...audioArgs, @@ -283,8 +285,8 @@ export async function startCameraStreamFfmpeg(device: ScryptedDevice & VideoCame else { const args = [ ...hideBanner, - ...decoderArgs, - ...videoInput.inputArguments, + ...videoDecoderArguments, + ...ffmpegInput.inputArguments, ...nullAudioInput, ...videoArgs, ...audioArgs, diff --git a/plugins/homekit/src/types/camera/camera-streaming.ts b/plugins/homekit/src/types/camera/camera-streaming.ts index 49bb1a668..63608638b 100644 --- a/plugins/homekit/src/types/camera/camera-streaming.ts +++ b/plugins/homekit/src/types/camera/camera-streaming.ts @@ -15,7 +15,6 @@ import { createSnapshotHandler } from '../camera/camera-snapshot'; import { DynamicBitrateSession } from './camera-dynamic-bitrate'; import { startCameraStreamFfmpeg } from './camera-streaming-ffmpeg'; import { CameraStreamingSession } from './camera-streaming-session'; -import { startCameraStreamSrtp } from './camera-streaming-srtp'; import { getStreamingConfiguration } from './camera-utils'; const { mediaManager } = sdk; @@ -184,7 +183,6 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame const { destination, dynamicBitrate, - transcodeStreaming, isLowBandwidth, isWatch, } = await getStreamingConfiguration(device, storage, request) @@ -193,7 +191,6 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame dynamicBitrate, isLowBandwidth, isWatch, - transcodeStreaming, destination, }); @@ -290,7 +287,6 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame console, storage, videoInput, - transcodeStreaming, session, () => killSession(request.sessionID)); } diff --git a/plugins/homekit/src/types/camera/camera-transcode.ts b/plugins/homekit/src/types/camera/camera-transcode.ts deleted file mode 100644 index 707eff290..000000000 --- a/plugins/homekit/src/types/camera/camera-transcode.ts +++ /dev/null @@ -1,6 +0,0 @@ -// request is used by the eval, do not remove. -export function evalRequest(value: string, request: any) { - if (value.startsWith('`')) - value = eval(value) as string; - return value.split(' '); -} diff --git a/plugins/homekit/src/types/camera/camera-utils.ts b/plugins/homekit/src/types/camera/camera-utils.ts index 46ba54f9e..6ad9182b1 100644 --- a/plugins/homekit/src/types/camera/camera-utils.ts +++ b/plugins/homekit/src/types/camera/camera-utils.ts @@ -1,5 +1,8 @@ import { MediaStreamDestination, ScryptedDevice, ScryptedInterface, VideoCamera } from "@scrypted/sdk"; import { H264Level, H264Profile, StartStreamRequest } from '../../hap'; +import sdk from '@scrypted/sdk'; + +const { log } = sdk; export function profileToFfmpeg(profile: H264Profile): string { if (profile === H264Profile.HIGH) @@ -46,6 +49,13 @@ export const ntpTime = () => { }; export async function getStreamingConfiguration(device: ScryptedDevice & VideoCamera, storage: Storage, request: StartStreamRequest) { + let adaptiveBitrate: string[] = []; + try { + adaptiveBitrate = JSON.parse(storage.getItem('adaptiveBitrate')); + } + catch (e) { + } + // Have only ever seen 20 and 60 sent here. 60 is remote stream and watch. const isLowBandwidth = request.audio.packet_time > 20; @@ -63,10 +73,8 @@ export async function getStreamingConfiguration(device: ScryptedDevice & VideoCa // watch will/should also be a low bandwidth device. if (isWatch) { - const watchStreamingMode = storage.getItem('watchStreamingMode'); return { - dynamicBitrate: canDynamicBitrate && watchStreamingMode === 'Adaptive Bitrate', - transcodeStreaming: watchStreamingMode === 'Transcode', + dynamicBitrate: canDynamicBitrate && adaptiveBitrate.includes('Apple Watch'), destination, isWatch, isLowBandwidth, @@ -74,31 +82,49 @@ export async function getStreamingConfiguration(device: ScryptedDevice & VideoCa } if (isLowBandwidth) { - let hubStreamingMode = storage.getItem('hubStreamingMode'); - - // 3/19/2022 migrate setting. - if (storage.getItem('transcodeStreamingHub') === 'true') { - if (!hubStreamingMode) - hubStreamingMode = 'Transcode'; - storage.removeItem('transcodeStreamingHub'); - } - return { - dynamicBitrate: canDynamicBitrate && hubStreamingMode === 'Adaptive Bitrate', - transcodeStreaming: hubStreamingMode === 'Transcode', + dynamicBitrate: canDynamicBitrate && adaptiveBitrate.includes('Remote Stream'), destination, isWatch, isLowBandwidth, } } - let transcodeStreaming = storage.getItem('transcodeStreaming'); - return { - dynamicBitrate: false, - transcodeStreaming: transcodeStreaming === 'true', + dynamicBitrate: canDynamicBitrate && adaptiveBitrate.includes('Local Stream'), destination, isWatch, isLowBandwidth, } } + +export function transcodingDebugModeWarning() { + console.warn('================================================================================='); + console.warn('Transcoding Debug Mode is enabled on this camera.'); + console.warn('This setting is used to diagnose camera issues, and should not be used long term.'); + console.warn('The HomeKit Readme contains proper camera configuration to avoid transcoding.'); + console.warn('More robust transcoding options are available within the Rebroadcast Plugin.'); + console.warn('================================================================================='); +} + +export function checkCompatibleCodec(console: Console, device: ScryptedDevice, videoCodec: String) { + if (!videoCodec) { + console.warn('============================================================================='); + console.warn('No video codec reported. This stream may fail. Enable the Rebroadcast Plugin.'); + console.warn('Stream compatibility can be diagnosed by enabling Transcoding Debug Mode.'); + console.warn('============================================================================='); + return; + } + const isDefinitelyNotH264 = videoCodec && videoCodec.toLowerCase().indexOf('h264') === -1; + if (isDefinitelyNotH264) { + const str = + console.error('============================================================================='); + console.error(`${device.name} video codec must be h264 but is ${videoCodec}.`); + console.error('This stream may fail. Read the instructions in the HomeKit Plugin'); + console.error('to properly configure your camera codec.'); + console.error('Stream compatibility can be diagnosed by enabling Transcoding Debug Mode.'); + console.error('============================================================================='); + + log.a(`${device.name} video codec must be h264 but is ${videoCodec}. This stream may fail. Read the instructions in the HomeKit Plugin to properly configure your camera codec.`); + } +} diff --git a/plugins/prebuffer-mixin/README.md b/plugins/prebuffer-mixin/README.md index 4b5d704ad..863f4036b 100644 --- a/plugins/prebuffer-mixin/README.md +++ b/plugins/prebuffer-mixin/README.md @@ -1,3 +1,23 @@ # Rebroadcast and Prebuffer for Scrypted This plugin maintains connections to all connected cameras, and buffers a small amount of recent video for instant replays. This instant replay is used by HomeKit Secure Video, as well as speeding up initial live stream load times. + +## Stream Setup + +The Rebroadcast Plugin will automatically select the best stream depending on the use. For example, a Unifi Camera has 3 available streams: `High`, `Medium`, and `Low`. Rebroadcast will automatically Prebuffer `High` for HomeKit Secure Video, and the stream selection will use the following defaults: + +High: `Local Stream` (HomeKit on LAN) and `Local Recording Stream` (NVR) +Medium: `Remote (Medium Resolution) Stream` (HomeKit on cellular) and `Remote Recording Stream` (HomeKit Secure Video) +Low: `Low Resolution Stream` for Apple Watch, Video Analysis + +Most cameras have at least 2 streams available and should be set up as follows: + +High: 1080p+ (2000 Kbps) +Medium: 720p (500 Kbps) +Low (if available): 320p (100 Kbps) + +The `Key Frame (IDR) Interval` should be set to `4` seconds. This setting is usually configured in frames. So if the camera frame rate is `30`, the interval would be `120`. If the camera frame rate is `15` the interval would be `60`. The value can be calculated as `IDR Interval = FPS * 4`. + +## Transcoding + +Some cameras may not allow configuration of the video codec (h264) or IDR Interval. The camera may also only have a single high bitrate stream which will fail to stream when viewing on low bandwidth remote connections. In this case, Transcoding should be enabled for `Remote Stream` and `Remote Recording Stream` to ensure there isn't a bandwidth issue. diff --git a/plugins/prebuffer-mixin/package.json b/plugins/prebuffer-mixin/package.json index 63f9c3adf..77e1478c0 100644 --- a/plugins/prebuffer-mixin/package.json +++ b/plugins/prebuffer-mixin/package.json @@ -25,6 +25,7 @@ "singleInstance": true, "type": "API", "interfaces": [ + "Settings", "MixinProvider", "BufferConverter" ] diff --git a/plugins/prebuffer-mixin/src/main.ts b/plugins/prebuffer-mixin/src/main.ts index c344c2be3..7a89d6459 100644 --- a/plugins/prebuffer-mixin/src/main.ts +++ b/plugins/prebuffer-mixin/src/main.ts @@ -3,7 +3,7 @@ import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-p import { startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session'; import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityTimer, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast'; import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster'; -import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers'; +import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers'; import { readLength } from '@scrypted/common/src/read-stream'; import { createRtspParser, H264_NAL_TYPE_IDR, findH264NaluType, RtspClient, RtspServer, RTSP_FRAME_MAGIC } from '@scrypted/common/src/rtsp-server'; import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils'; @@ -18,6 +18,7 @@ import net from 'net'; import { Duplex } from 'stream'; import { connectRFC4571Parser, RtspChannelCodecMapping, startRFC4571Parser } from './rfc4571'; import { createStreamSettings, getPrebufferedStreams } from './stream-settings'; +import { getH264EncoderArgs, LIBX264_ENCODER_TITLE } from '@scrypted/common/src/ffmpeg-hardware-acceleration'; const { mediaManager, log, systemManager, deviceManager } = sdk; @@ -235,7 +236,7 @@ class PrebufferSession { const elapsed = Date.now() - start; const bitrate = Math.round(total / elapsed * 8); - const group = this.streamName ? `Rebroadcast: ${this.streamName}` : 'Rebroadcast'; + const group = this.streamName ? `Stream: ${this.streamName}` : 'Stream'; settings.push( { @@ -261,7 +262,6 @@ class PrebufferSession { STRING_DEFAULT, 'MPEG-TS', 'RTSP', - // 'RTSP+MP4', ], key: this.rebroadcastModeKey, value: this.storage.getItem(this.rebroadcastModeKey) || STRING_DEFAULT, @@ -724,9 +724,8 @@ class PrebufferSession { id: this.streamId, refresh: false, }) - .then(async (stream) => { + .then(async (ffmpegInput) => { const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS; - const ffmpegInput = await mediaManager.convertMediaObjectToJSON(stream, ScryptedMimeTypes.FFmpegInput); ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' ')); const mp4Session = await startFFMPegFragmentedMP4Session(ffmpegInput.inputArguments, acodec, vcodec, this.console); @@ -853,11 +852,7 @@ class PrebufferSession { updateIdr(); } else if (chunk.type === 'h264') { - // only compute idr every 30 seconds - if (Date.now() > prevIdr + 30000) - prevIdr = undefined; - if ((prevIdr === undefined || this.detectedIdrInterval === undefined) - && findH264NaluType(chunk, H264_NAL_TYPE_IDR)) { + if (findH264NaluType(chunk, H264_NAL_TYPE_IDR)) { // only update the rtsp computed idr once a minute. // per packet bitscan is not great. updateIdr(); @@ -994,7 +989,7 @@ class PrebufferSession { }) } - async getVideoStream(options?: RequestMediaStreamOptions): Promise { + async getVideoStream(options?: RequestMediaStreamOptions) { if (options?.refresh === false && !this.parserSessionPromise) throw new Error('Stream is currently unavailable and will not be started for this request. RequestMediaStreamOptions.refresh === false'); @@ -1123,7 +1118,7 @@ class PrebufferSession { mediaStreamOptions, } - return this.mixin.createMediaObject(ffmpegInput, ScryptedMimeTypes.FFmpegInput); + return ffmpegInput; } } @@ -1133,7 +1128,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase) { + constructor(public plugin: RebroadcastPlugin, options: SettingsMixinDeviceOptions) { super(options); this.delayStart(); @@ -1152,13 +1147,16 @@ class PrebufferMixin extends SettingsMixinDeviceBase session.kill()); } @@ -1369,12 +1386,26 @@ function millisUntilMidnight() { return (midnight.getTime() - new Date().getTime()); } -class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider, BufferConverter { +class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings { storageSettings = new StorageSettings(this, { rebroadcastPort: { title: 'Rebroadcast Port', description: 'The port of the RTSP server that will rebroadcast your streams.', type: 'number', + }, + remoteStreamingBitrate: { + title: 'Remote Streaming Bitrate', + type: 'number', + defaultValue: 500000, + description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.', + }, + h264EncoderArguments: { + title: 'H264 Encoder Arguments', + description: 'FFmpeg arguments used to encode h264 video. This is not camera specific and is used to setup the hardware accelerated encoder on your Scrypted server. This setting will only be used when transcoding is enabled on a camera.', + choices: Object.keys(getH264EncoderArgs()), + defaultValue: getH264EncoderArgs()[LIBX264_ENCODER_TITLE].join(' '), + combobox: true, + mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getH264EncoderArgs()[LIBX264_ENCODER_TITLE]?.join(' '), } }); rtspServer: net.Server; @@ -1411,6 +1442,14 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider this.startRtspServer(); } + getSettings(): Promise { + return this.storageSettings.getSettings(); + } + + putSetting(key: string, value: SettingValue): Promise { + return this.storageSettings.putSetting(key, value); + } + startRtspServer() { closeQuiet(this.rtspServer); @@ -1555,4 +1594,4 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider } } -export default new PrebufferProvider(); +export default new RebroadcastPlugin(); diff --git a/plugins/prebuffer-mixin/src/rfc4571.ts b/plugins/prebuffer-mixin/src/rfc4571.ts index 491a055fc..f68603b3d 100644 --- a/plugins/prebuffer-mixin/src/rfc4571.ts +++ b/plugins/prebuffer-mixin/src/rfc4571.ts @@ -60,8 +60,8 @@ export async function startRFC4571Parser(console: Console, socket: Readable, sdp height: number; }; - const sprop = parsedSdp.msections.find(msection => msection.codec === 'h264') - .fmtp?.[0]?.parameters?.['sprop-parameter-sets']; + const sprop = videoSection + ?.fmtp?.[0]?.parameters?.['sprop-parameter-sets']; const sdpSps = sprop?.split(',')?.[0]; // const sdpPps = sprop?.split(',')?.[1]; diff --git a/plugins/prebuffer-mixin/src/stream-settings.ts b/plugins/prebuffer-mixin/src/stream-settings.ts index 5ff9f0771..238350749 100644 --- a/plugins/prebuffer-mixin/src/stream-settings.ts +++ b/plugins/prebuffer-mixin/src/stream-settings.ts @@ -1,3 +1,4 @@ +import { getH264DecoderArgs } from "@scrypted/common/src/ffmpeg-hardware-acceleration"; import { StorageSetting, StorageSettings } from "@scrypted/common/src/settings"; import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk"; @@ -54,7 +55,7 @@ export function createStreamSettings(device: MixinDeviceBase) { const streamTypes = getStreamTypes({ defaultStream: { title: 'Local Stream', - description: 'The media stream to use when streaming on your local network. This is the default stream. This stream should be prebuffered. Recommended resolution: 1920x1080 to 4K.', + description: 'The media stream to use when streaming on your local network. This stream should be prebuffered. Recommended resolution: 1920x1080 to 4K.', hide: true, prefersPrebuffer: true, preferredResolution: 3840 * 2160, @@ -97,9 +98,37 @@ export function createStreamSettings(device: MixinDeviceBase) { hide: false, }, ...streamTypes, + transcodeStreams: { + group: 'Transcoding', + title: 'Transcode Streams', + description: 'The media streams to transcode. Transcoding audio and video is not recommended and should only be used when necessary. The Rebroadcast Plugin manages the system-wide Transcode settings and as well as a Readme for optimal configuration.', + multiple: true, + choices: Object.values(streamTypes).map(st => st.title), + }, + // 3/6/2022 + // Ran into an issue where the RTSP source had SPS/PPS in the SDP, + // and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames + // unless this flag is used. + // 3/7/2022 + // This flag was enabled by default, but I believe this is causing issues with some users. + // Make it a setting. + missingCodecParameters: { + group: 'Transcoding', + title: 'Add H264 Extra Data', + description: 'Some cameras do not include H264 extra data in the stream and this causes live streaming to always fail (but recordings may be working). This is a inexpensive video filter and does not perform a transcode. Enable this setting only as necessary.', + type: 'boolean', + }, + videoDecoderArguments: { + group: 'Transcoding', + title: 'Video Decoder Arguments', + description: 'FFmpeg arguments used to decode input video when transcoding a stream.', + placeholder: '-hwaccel auto', + choices: Object.keys(getH264DecoderArgs()), + combobox: true, + mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue, + } }); - function getDefaultMediaStream(v: StreamStorageSetting, msos: ResponseMediaStreamOptions[]) { const enabledStreams = getPrebufferedStreams(storageSettings, msos); const prebufferPreferenceStreams = v.prefersPrebuffer && enabledStreams?.length > 0 ? enabledStreams : msos; @@ -116,6 +145,7 @@ export function createStreamSettings(device: MixinDeviceBase) { stream = getDefaultMediaStream(v, msos); } return { + title: streamTypes[key].title, isDefault, stream, }; diff --git a/sdk/gen/types.input.ts b/sdk/gen/types.input.ts index d23bbaa4d..b87f35e97 100644 --- a/sdk/gen/types.input.ts +++ b/sdk/gen/types.input.ts @@ -991,6 +991,9 @@ export interface MediaStreamUrl { } export interface FFmpegInput extends MediaStreamUrl { inputArguments?: string[]; + h264EncoderArguments?: string[]; + videoDecoderArguments?: string[]; + h264FilterArguments?: string[]; } /** * DeviceManager is the interface used by DeviceProvider to report new devices, device states, and device events to Scrypted. diff --git a/sdk/scrypted_python/scrypted_sdk/types.py b/sdk/scrypted_python/scrypted_sdk/types.py index 56fc82f93..07ea5e6c7 100644 --- a/sdk/scrypted_python/scrypted_sdk/types.py +++ b/sdk/scrypted_python/scrypted_sdk/types.py @@ -258,9 +258,12 @@ class EventListenerRegister(TypedDict): class FFmpegInput(TypedDict): container: str + h264EncoderArguments: list[str] + h264FilterArguments: list[str] inputArguments: list[str] mediaStreamOptions: ResponseMediaStreamOptions url: str + videoDecoderArguments: list[str] pass class FanState(TypedDict): diff --git a/sdk/types/index.d.ts b/sdk/types/index.d.ts index 61f7d685a..861a06d1e 100644 --- a/sdk/types/index.d.ts +++ b/sdk/types/index.d.ts @@ -1061,6 +1061,9 @@ export interface MediaStreamUrl { } export interface FFmpegInput extends MediaStreamUrl { inputArguments?: string[]; + h264EncoderArguments?: string[]; + videoDecoderArguments?: string[]; + h264FilterArguments?: string[]; } /** * DeviceManager is the interface used by DeviceProvider to report new devices, device states, and device events to Scrypted. diff --git a/sdk/types/index.ts b/sdk/types/index.ts index 808ab6324..3a0c13771 100644 --- a/sdk/types/index.ts +++ b/sdk/types/index.ts @@ -1692,6 +1692,9 @@ export interface MediaStreamUrl { } export interface FFmpegInput extends MediaStreamUrl { inputArguments?: string[]; + h264EncoderArguments?: string[]; + videoDecoderArguments?: string[]; + h264FilterArguments?: string[]; } /** * DeviceManager is the interface used by DeviceProvider to report new devices, device states, and device events to Scrypted. diff --git a/sdk/types/scrypted_python/scrypted_sdk/types.py b/sdk/types/scrypted_python/scrypted_sdk/types.py index 56fc82f93..07ea5e6c7 100644 --- a/sdk/types/scrypted_python/scrypted_sdk/types.py +++ b/sdk/types/scrypted_python/scrypted_sdk/types.py @@ -258,9 +258,12 @@ class EventListenerRegister(TypedDict): class FFmpegInput(TypedDict): container: str + h264EncoderArguments: list[str] + h264FilterArguments: list[str] inputArguments: list[str] mediaStreamOptions: ResponseMediaStreamOptions url: str + videoDecoderArguments: list[str] pass class FanState(TypedDict):