Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RSDK-6469] Set x264 bitrate based on resolution and fps #4769

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions gostream/codec/x264/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ type encoder struct {
logger logging.Logger
}

// Gives suitable results. Probably want to make this configurable this in the future.
const bitrate = 3_200_000

// NewEncoder returns an x264 encoder that can encode images of the given width and height. It will
// also ensure that it produces key frames at the given interval.
func NewEncoder(width, height, keyFrameInterval int, logger logging.Logger) (ourcodec.VideoEncoder, error) {
Expand All @@ -33,8 +30,8 @@ func NewEncoder(width, height, keyFrameInterval int, logger logging.Logger) (our
return nil, err
}
builder = &params
params.BitRate = bitrate
params.KeyFrameInterval = keyFrameInterval
params.BitRate = calcBitrateFromResolution(width, height, float32(params.KeyFrameInterval))

codec, err := builder.BuildVideoEncoder(enc, prop.Media{
Video: prop.Video{
Expand Down
24 changes: 24 additions & 0 deletions gostream/codec/x264/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import (
"go.viam.com/rdk/logging"
)

const (
encodeCompressionRatio = 0.15 // bits per pixel when encoded
// For very small resolutions, we need to ensure that the vbv buffer size is large enough to
// handle frame bursts. This is the minimum bitrate that we can use without causing the encoder
// to spew out warnings about the buffer size being too small.
minBitrate = 300_000 // 300kbps
// Setting a reasonable max bitrate to prevent the encoder from using too much bandwidth.
// 4K resolution at 20fps is around 24.8Mbps.
maxBitrate = 25_000_000 // 25Mbps
)

// DefaultStreamConfig configures x264 as the encoder for a stream.
var DefaultStreamConfig gostream.StreamConfig

Expand All @@ -27,3 +38,16 @@ func (f *factory) New(width, height, keyFrameInterval int, logger logging.Logger
func (f *factory) MIMEType() string {
return "video/H264"
}

// calcBitrateFromResolution calculates the bitrate based on the given resolution and framerate.
func calcBitrateFromResolution(width, height int, framerate float32) int {
bitrate := int(float32(width) * float32(height) * framerate * encodeCompressionRatio)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does bitrate scale linearly with width and height? Apparently the answer is complicated https://www.reddit.com/r/explainlikeimfive/comments/hlrwcu/eli5_what_is_the_relationship_between_bit_rate/

Copy link
Member Author

@seanavery seanavery Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently scaling the bitrate linearly with the number of pixels.

Interesting graphs in the thread that show logarithmic quality gains with increasing bitrate for a given size/fps. Will try to verify that we are hitting these sweet-spots. The amount of scene change also comes into play here so it gets quite complicated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at the example bitrates in the description we seem to be pretty bang on to the recommended settings linked in the jira ticket: https://support.google.com/youtube/answer/2853702?hl=en

// This accounts for zero bitrates too.
if bitrate < minBitrate {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if bitrate < minBitrate {
// this accounts for zero bitrates too
if bitrate < minBitrate {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, added comment.

return minBitrate
}
if bitrate > maxBitrate {
return maxBitrate
}
return bitrate
}
19 changes: 19 additions & 0 deletions robot/web/stream/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,18 @@ func (server *Server) AddNewStreams(ctx context.Context) error {
server.logger.Warn("video streaming not supported on Windows yet")
break
}
// Attempt to look up the framerate for the camera. If the framerate is not available, we'll
// end up with a framerate of 0. This is fine as gostream will default to 30fps in this case.
framerate, err := server.getFramerateFromCamera(name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this new logic break streaming for cameras that don't provide framerate in getProperties?

Copy link
Member Author

@seanavery seanavery Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add comment here -- encoder pipeline will default to 30fps in bitrate calculation (the same as the key-frame interval) if it cant pick up framerate.

if err != nil {
server.logger.Debugf("error getting framerate from camera %q: %v", name, err)
}
// We walk the updated set of `videoSources` and ensure all of the sources are "created" and
// "started".
config := gostream.StreamConfig{
Name: name,
VideoEncoderFactory: server.streamConfig.VideoEncoderFactory,
TargetFrameRate: framerate,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this is 0? Can we add a test?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encoder pipeline will default to 30fps in bitrate calculation (the same as the key-frame interval) if it cant pick up framerate.

Will see about adding a test for this.

}
// Call `createStream`. `createStream` is responsible for first checking if the stream
// already exists. If it does, it skips creating a new stream and we continue to the next source.
Expand Down Expand Up @@ -758,6 +765,18 @@ func (server *Server) startAudioStream(ctx context.Context, source gostream.Audi
})
}

func (server *Server) getFramerateFromCamera(name string) (int, error) {
cam, err := camera.FromRobot(server.robot, name)
if err != nil {
return 0, fmt.Errorf("failed to get camera from robot: %w", err)
}
props, err := cam.Properties(context.Background())
if err != nil {
return 0, fmt.Errorf("failed to get camera properties: %w", err)
}
return int(props.FrameRate), nil
}

// GenerateResolutions takes the original width and height of an image and returns
// a list of the original resolution with 4 smaller width/height options that maintain
// the same aspect ratio.
Expand Down
Loading