github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/ingress/validation.go (about) 1 // Copyright 2023 LiveKit, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ingress 16 17 import ( 18 "github.com/livekit/protocol/livekit" 19 ) 20 21 // This validates that ingress options have no consistency issues and provide enough parameters 22 // to be usable by the ingress service. Options that pass this test may still need some fields to be poulated with default values 23 // before being used in a media pipeline. 24 25 func Validate(info *livekit.IngressInfo) error { 26 if info == nil { 27 return ErrInvalidIngress("missing IngressInfo") 28 } 29 30 // For now, require a room to be set. We should eventually allow changing the room on an active ingress 31 if info.RoomName == "" { 32 return ErrInvalidIngress("no room name") 33 } 34 35 return ValidateForSerialization(info) 36 } 37 38 // Sparse info with not all required fields populated are acceptable for serialization, provided values are consistent 39 func ValidateForSerialization(info *livekit.IngressInfo) error { 40 if info == nil { 41 return ErrInvalidIngress("missing IngressInfo") 42 } 43 44 if info.InputType != livekit.IngressInput_RTMP_INPUT && info.InputType != livekit.IngressInput_WHIP_INPUT && info.InputType != livekit.IngressInput_URL_INPUT { 45 return ErrInvalidIngress("unsupported input type") 46 } 47 48 // Validate source 49 switch info.InputType { 50 case livekit.IngressInput_RTMP_INPUT, 51 livekit.IngressInput_WHIP_INPUT: 52 if info.StreamKey == "" { 53 return ErrInvalidIngress("no stream key") 54 } 55 case livekit.IngressInput_URL_INPUT: 56 if info.Url == "" { 57 return ErrInvalidIngress("no source URL") 58 } 59 } 60 61 if info.ParticipantIdentity == "" { 62 return ErrInvalidIngress("no participant identity") 63 } 64 65 err := ValidateBypassTranscoding(info) 66 if err != nil { 67 return err 68 } 69 70 err = ValidateEnableTranscoding(info) 71 if err != nil { 72 return err 73 } 74 75 err = ValidateVideoOptionsConsistency(info.Video) 76 if err != nil { 77 return err 78 } 79 80 err = ValidateAudioOptionsConsistency(info.Audio) 81 if err != nil { 82 return err 83 } 84 85 return nil 86 87 } 88 89 func ValidateBypassTranscoding(info *livekit.IngressInfo) error { 90 if !info.BypassTranscoding { 91 return nil 92 } 93 94 if info.InputType != livekit.IngressInput_WHIP_INPUT { 95 return NewInvalidTranscodingBypassError("bypassing transcoding impossible with selected input type") 96 } 97 98 videoOptions := info.Video 99 if videoOptions != nil && videoOptions.EncodingOptions != nil { 100 return NewInvalidTranscodingBypassError("video encoding options must be empty if transcoding bypass is selected") 101 } 102 103 audioOptions := info.Audio 104 if audioOptions != nil && audioOptions.EncodingOptions != nil { 105 return NewInvalidTranscodingBypassError("audio encoding options must be empty if transcoding bypass is selected") 106 } 107 108 return nil 109 } 110 111 func ValidateEnableTranscoding(info *livekit.IngressInfo) error { 112 var enableTranscoding bool 113 if info.InputType == livekit.IngressInput_WHIP_INPUT { 114 enableTranscoding = false 115 if info.EnableTranscoding != nil { 116 enableTranscoding = *info.EnableTranscoding 117 } 118 } else { 119 enableTranscoding = true 120 if info.EnableTranscoding != nil && *info.EnableTranscoding == false { 121 return NewInvalidTranscodingBypassError("bypassing transcoding impossible with selected input type") 122 } 123 } 124 125 if !enableTranscoding { 126 videoOptions := info.Video 127 if videoOptions != nil && videoOptions.EncodingOptions != nil { 128 return NewInvalidTranscodingBypassError("video encoding options must be empty if transcoding is disabled") 129 } 130 131 audioOptions := info.Audio 132 if audioOptions != nil && audioOptions.EncodingOptions != nil { 133 return NewInvalidTranscodingBypassError("audio encoding options must be empty if transcoding is disabled") 134 } 135 } 136 137 return nil 138 } 139 140 func ValidateVideoOptionsConsistency(options *livekit.IngressVideoOptions) error { 141 if options == nil { 142 return nil 143 } 144 145 switch options.Source { 146 case livekit.TrackSource_UNKNOWN, 147 livekit.TrackSource_CAMERA, 148 livekit.TrackSource_SCREEN_SHARE: 149 // continue 150 default: 151 return NewInvalidVideoParamsError("invalid track source") 152 } 153 154 switch o := options.EncodingOptions.(type) { 155 case nil: 156 // Default, continue 157 case *livekit.IngressVideoOptions_Preset: 158 _, ok := livekit.IngressVideoEncodingPreset_name[int32(o.Preset)] 159 if !ok { 160 return NewInvalidVideoParamsError("invalid preset") 161 } 162 163 case *livekit.IngressVideoOptions_Options: 164 err := ValidateVideoEncodingOptionsConsistency(o.Options) 165 if err != nil { 166 return err 167 } 168 } 169 170 return nil 171 } 172 173 func ValidateVideoEncodingOptionsConsistency(options *livekit.IngressVideoEncodingOptions) error { 174 if options == nil { 175 return NewInvalidVideoParamsError("empty options") 176 } 177 178 if options.FrameRate < 0 { 179 return NewInvalidVideoParamsError("invalid framerate") 180 } 181 182 switch options.VideoCodec { 183 case livekit.VideoCodec_DEFAULT_VC, 184 livekit.VideoCodec_H264_BASELINE, 185 livekit.VideoCodec_VP8: 186 // continue 187 default: 188 return NewInvalidVideoParamsError("video codec unsupported") 189 } 190 191 layersByQuality := make(map[livekit.VideoQuality]*livekit.VideoLayer) 192 193 for _, layer := range options.Layers { 194 if layer.Height == 0 || layer.Width == 0 { 195 return ErrInvalidOutputDimensions 196 } 197 198 if layer.Width%2 == 1 { 199 return ErrInvalidIngress("layer width must be even") 200 } 201 202 if layer.Height%2 == 1 { 203 return ErrInvalidIngress("layer height must be even") 204 } 205 206 if _, ok := layersByQuality[layer.Quality]; ok { 207 return NewInvalidVideoParamsError("more than one layer with the same quality level") 208 } 209 layersByQuality[layer.Quality] = layer 210 } 211 212 var oldW, oldH uint32 213 for q := livekit.VideoQuality_LOW; q <= livekit.VideoQuality_HIGH; q++ { 214 layer, ok := layersByQuality[q] 215 if !ok { 216 continue 217 } 218 219 if layer.Height < oldH { 220 return NewInvalidVideoParamsError("video layers do not have increasing height with increasing quality") 221 } 222 if layer.Width < oldW { 223 return NewInvalidVideoParamsError("video layers do not have increasing width with increasing quality") 224 } 225 oldW = layer.Width 226 oldH = layer.Height 227 } 228 229 return nil 230 } 231 232 func ValidateAudioOptionsConsistency(options *livekit.IngressAudioOptions) error { 233 if options == nil { 234 return nil 235 } 236 237 switch options.Source { 238 case livekit.TrackSource_UNKNOWN, 239 livekit.TrackSource_MICROPHONE, 240 livekit.TrackSource_SCREEN_SHARE_AUDIO: 241 // continue 242 default: 243 return NewInvalidAudioParamsError("invalid track source") 244 } 245 246 switch o := options.EncodingOptions.(type) { 247 case nil: 248 // Default, continue 249 case *livekit.IngressAudioOptions_Preset: 250 _, ok := livekit.IngressAudioEncodingPreset_name[int32(o.Preset)] 251 if !ok { 252 return NewInvalidAudioParamsError("invalid preset") 253 } 254 255 case *livekit.IngressAudioOptions_Options: 256 err := ValidateAudioEncodingOptionsConsistency(o.Options) 257 if err != nil { 258 return err 259 } 260 } 261 262 return nil 263 } 264 265 func ValidateAudioEncodingOptionsConsistency(options *livekit.IngressAudioEncodingOptions) error { 266 if options == nil { 267 return NewInvalidAudioParamsError("empty options") 268 } 269 270 switch options.AudioCodec { 271 case livekit.AudioCodec_DEFAULT_AC, 272 livekit.AudioCodec_OPUS: 273 // continue 274 default: 275 return NewInvalidAudioParamsError("audio codec unsupported") 276 } 277 278 if options.Channels > 2 { 279 return NewInvalidAudioParamsError("invalid audio channel count") 280 } 281 282 return nil 283 }