livekit-protocol 0.7.5

Livekit protocol and utilities for the Rust SDK
Documentation
// 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.Enabled != nil && !*info.Enabled {
			return ErrInvalidIngress("disabled non reusable ingress")
		}
	}

	if info.ParticipantIdentity == "" {
		return ErrInvalidIngress("no participant identity")
	}

	err := ValidateBypassTranscoding(info)
	if err != nil {
		return err
	}

	err = ValidateEnableTranscoding(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 ValidateEnableTranscoding(info *livekit.IngressInfo) error {
	var enableTranscoding bool
	if info.InputType == livekit.IngressInput_WHIP_INPUT {
		enableTranscoding = false
		if info.EnableTranscoding != nil {
			enableTranscoding = *info.EnableTranscoding
		}
	} else {
		enableTranscoding = true
		if info.EnableTranscoding != nil && *info.EnableTranscoding == false {
			return NewInvalidTranscodingBypassError("bypassing transcoding impossible with selected input type")
		}
	}

	if !enableTranscoding {
		videoOptions := info.Video
		if videoOptions != nil && videoOptions.EncodingOptions != nil {
			return NewInvalidTranscodingBypassError("video encoding options must be empty if transcoding is disabled")
		}

		audioOptions := info.Audio
		if audioOptions != nil && audioOptions.EncodingOptions != nil {
			return NewInvalidTranscodingBypassError("audio encoding options must be empty if transcoding is disabled")
		}
	}

	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
}