Files
protocol/ingress/validation.go
Benjamin Pracht 14b7eb70d2 Update ingress layer validation logic to make sure both dimensions increase for each layer (#447)
This is a preamble to changing the ingress logic to boxing the input video into the layer dimension, instead of resizing to the layer dimensions regardless of aspect ratio.

This may cause some existing ingress to stop working.
2023-08-11 16:13:53 -07:00

250 lines
6.5 KiB
Go

// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ingress
import (
"github.com/livekit/protocol/livekit"
)
// This validates that ingress options have no consistency issues and provide enough parameters
// to be usable by the ingress service. Options that pass this test may still need some fields to be poulated with default values
// before being used in a media pipeline.
func Validate(info *livekit.IngressInfo) error {
if info == nil {
return ErrInvalidIngress("missing IngressInfo")
}
// For now, require a room to be set. We should eventually allow changing the room on an active ingress
if info.RoomName == "" {
return ErrInvalidIngress("no room name")
}
return ValidateForSerialization(info)
}
// Sparse info with not all required fields populated are acceptable for serialization, provided values are consistent
func ValidateForSerialization(info *livekit.IngressInfo) error {
if info == nil {
return ErrInvalidIngress("missing IngressInfo")
}
if info.InputType != livekit.IngressInput_RTMP_INPUT && info.InputType != livekit.IngressInput_WHIP_INPUT && info.InputType != livekit.IngressInput_URL_INPUT {
return ErrInvalidIngress("unsupported input type")
}
// Validate source
switch info.InputType {
case livekit.IngressInput_RTMP_INPUT,
livekit.IngressInput_WHIP_INPUT:
if info.StreamKey == "" {
return ErrInvalidIngress("no stream key")
}
case livekit.IngressInput_URL_INPUT:
if info.Url == "" {
return ErrInvalidIngress("no source URL")
}
}
if info.ParticipantIdentity == "" {
return ErrInvalidIngress("no participant identity")
}
err := ValidateBypassTranscoding(info)
if err != nil {
return err
}
err = ValidateVideoOptionsConsistency(info.Video)
if err != nil {
return err
}
err = ValidateAudioOptionsConsistency(info.Audio)
if err != nil {
return err
}
return nil
}
func ValidateBypassTranscoding(info *livekit.IngressInfo) error {
if !info.BypassTranscoding {
return nil
}
if info.InputType != livekit.IngressInput_WHIP_INPUT {
return NewInvalidTranscodingBypassError("bypassing transcoding impossible with selected input type")
}
videoOptions := info.Video
if videoOptions != nil && videoOptions.EncodingOptions != nil {
return NewInvalidTranscodingBypassError("video encoding options must be empty if transcoding bypass is selected")
}
audioOptions := info.Audio
if audioOptions != nil && audioOptions.EncodingOptions != nil {
return NewInvalidTranscodingBypassError("audio encoding options must be empty if transcoding bypass is selected")
}
return nil
}
func ValidateVideoOptionsConsistency(options *livekit.IngressVideoOptions) error {
if options == nil {
return nil
}
switch options.Source {
case livekit.TrackSource_UNKNOWN,
livekit.TrackSource_CAMERA,
livekit.TrackSource_SCREEN_SHARE:
// continue
default:
return NewInvalidVideoParamsError("invalid track source")
}
switch o := options.EncodingOptions.(type) {
case nil:
// Default, continue
case *livekit.IngressVideoOptions_Preset:
_, ok := livekit.IngressVideoEncodingPreset_name[int32(o.Preset)]
if !ok {
return NewInvalidVideoParamsError("invalid preset")
}
case *livekit.IngressVideoOptions_Options:
err := ValidateVideoEncodingOptionsConsistency(o.Options)
if err != nil {
return err
}
}
return nil
}
func ValidateVideoEncodingOptionsConsistency(options *livekit.IngressVideoEncodingOptions) error {
if options == nil {
return NewInvalidVideoParamsError("empty options")
}
if options.FrameRate < 0 {
return NewInvalidVideoParamsError("invalid framerate")
}
switch options.VideoCodec {
case livekit.VideoCodec_DEFAULT_VC,
livekit.VideoCodec_H264_BASELINE,
livekit.VideoCodec_VP8:
// continue
default:
return NewInvalidVideoParamsError("video codec unsupported")
}
layersByQuality := make(map[livekit.VideoQuality]*livekit.VideoLayer)
for _, layer := range options.Layers {
if layer.Height == 0 || layer.Width == 0 {
return ErrInvalidOutputDimensions
}
if layer.Width%2 == 1 {
return ErrInvalidIngress("layer width must be even")
}
if layer.Height%2 == 1 {
return ErrInvalidIngress("layer height must be even")
}
if _, ok := layersByQuality[layer.Quality]; ok {
return NewInvalidVideoParamsError("more than one layer with the same quality level")
}
layersByQuality[layer.Quality] = layer
}
var oldW, oldH uint32
for q := livekit.VideoQuality_LOW; q <= livekit.VideoQuality_HIGH; q++ {
layer, ok := layersByQuality[q]
if !ok {
continue
}
if layer.Height < oldH {
return NewInvalidVideoParamsError("video layers do not have increasing height with increasing quality")
}
if layer.Width < oldW {
return NewInvalidVideoParamsError("video layers do not have increasing width with increasing quality")
}
oldW = layer.Width
oldH = layer.Height
}
return nil
}
func ValidateAudioOptionsConsistency(options *livekit.IngressAudioOptions) error {
if options == nil {
return nil
}
switch options.Source {
case livekit.TrackSource_UNKNOWN,
livekit.TrackSource_MICROPHONE,
livekit.TrackSource_SCREEN_SHARE_AUDIO:
// continue
default:
return NewInvalidAudioParamsError("invalid track source")
}
switch o := options.EncodingOptions.(type) {
case nil:
// Default, continue
case *livekit.IngressAudioOptions_Preset:
_, ok := livekit.IngressAudioEncodingPreset_name[int32(o.Preset)]
if !ok {
return NewInvalidAudioParamsError("invalid preset")
}
case *livekit.IngressAudioOptions_Options:
err := ValidateAudioEncodingOptionsConsistency(o.Options)
if err != nil {
return err
}
}
return nil
}
func ValidateAudioEncodingOptionsConsistency(options *livekit.IngressAudioEncodingOptions) error {
if options == nil {
return NewInvalidAudioParamsError("empty options")
}
switch options.AudioCodec {
case livekit.AudioCodec_DEFAULT_AC,
livekit.AudioCodec_OPUS:
// continue
default:
return NewInvalidAudioParamsError("audio codec unsupported")
}
if options.Channels > 2 {
return NewInvalidAudioParamsError("invalid audio channel count")
}
return nil
}