Overview
Pre-encoded video publishing sends video that your application or device already encoded to a LiveKit room. The SDK packetizes each encoded frame and publishes without needing to transcode.
Use this path for a hardware encoder, an existing media pipeline, or a camera that produces encoded video. Use a regular video source when your application produces raw pixel buffers.
Pre-encoded ingest is different from the following features:
- SDK hardware encoding accepts raw frames and selects a hardware encoder when one is available. To learn more, see Hardware encoder support.
- LiveKit Ingress imports RTMP, WHIP, HTTP media, and other external streams through a separate service. This service runs in the cloud. To learn more, see Ingress overview.
SDK support
The following SDKs provide a public API for pre-encoded video:
| SDK | Minimum version | Publishing API |
|---|---|---|
| Rust | livekit 0.7.53 | NativeVideoSource::new_encoded and capture_encoded_frame |
| C++ | 1.11.0 | EncodedVideoSource and captureFrame |
| Go server SDK | v2.4.0 | NewLocalSampleTrack and WriteSample, or WriteRTP |
Input requirements
Submit one complete encoded access unit for each frame. An access unit contains all encoded data for one displayed frame.
Each access unit requires the following information:
- Payload: The encoded bytes for one complete frame.
- Codec: H.264, H.265, VP8, VP9, or AV1.
- Frame type: A key frame or a delta frame.
- Timestamp: The capture time for the frame.
- Resolution: The encoded width and height.
The codec must stay the same for the life of the source. It must also match the codec in the track publish options.
Codec support depends on the subscribers and their platforms. Make sure that every subscriber can decode the selected codec.
The H.264 examples use Annex B access units. Include the current SPS and PPS data with each key frame so that a new subscriber can initialize its decoder.
Pre-encoded sources publish one spatial layer. The Rust and C++ examples disable simulcast because the SDK cannot create more encoded layers from one access unit.
Publish pre-encoded video
The following examples publish one H.264 video track. Replace the placeholder capture loop with the output from your encoder.
use livekit::options::{TrackPublishOptions, VideoCodec, VideoEncoderBackend};use livekit::prelude::*;use livekit::webrtc::{video_frame::{EncodedFrameType, EncodedVideoCodec, EncodedVideoFrame},video_source::{native::NativeVideoSource, RtcVideoSource, VideoResolution}};const WIDTH: u32 = 1920;const HEIGHT: u32 = 1080;let resolution = VideoResolution { width: WIDTH, height: HEIGHT };let source = NativeVideoSource::new_encoded(resolution.clone());let track = LocalVideoTrack::create_video_track("camera",RtcVideoSource::Native(source.clone()),);let options = TrackPublishOptions {video_codec: VideoCodec::H264,video_encoder: VideoEncoderBackend::PreEncoded,simulcast: false,source: TrackSource::Camera,..Default::default()};room.local_participant().publish_track(LocalTrack::Video(track), options).await?;loop {// Read one complete access unit from your encoder.let (payload, timestamp_us, is_keyframe) = next_access_unit()?;let frame = EncodedVideoFrame {codec: EncodedVideoCodec::H264,payload: &payload,timestamp_us,frame_type: if is_keyframe {EncodedFrameType::Key} else {EncodedFrameType::Delta},resolution: resolution.clone(),frame_metadata: None,};if !source.capture_encoded_frame(&frame) {// The source did not accept this frame.continue;}if source.take_keyframe_request() {// Ask your encoder to produce a key frame.}if let Some(target) = source.take_rate_control_request() {// Update your encoder with target.target_bitrate_bps// and target.framerate_fps.}}
The livekit-capture crate also provides EncodedVideoPump. This crate is in Developer Preview.
#include "livekit/encoded_video_source.h"#include "livekit/livekit.h"#include <memory>#include <utility>constexpr int kWidth = 1920;constexpr int kHeight = 1080;auto source = std::make_shared<livekit::EncodedVideoSource>(livekit::VideoCodec::H264, kWidth, kHeight);auto local_participant = room->localParticipant().lock();if (!local_participant) {return;}// This helper selects the pre-encoded backend, matches the source codec,// and disables simulcast.auto track = local_participant->publishVideoTrack("camera", source, livekit::TrackSource::SOURCE_CAMERA);while (true) {// Read one complete access unit from your encoder.auto access_unit = nextAccessUnit();livekit::EncodedVideoSource::Frame frame;frame.data = std::move(access_unit.data);frame.is_keyframe = access_unit.is_keyframe;frame.timestamp_us = access_unit.timestamp_us;if (!source->captureFrame(frame)) {// The source did not accept this frame.continue;}const auto feedback = source->takeFeedback();if (feedback.keyframe_requested) {// Ask your encoder to produce a key frame.}if (feedback.rate_control) {// Update your encoder with target_bitrate_bps and framerate_fps.}}
captureFrame copies the access-unit payload before the function returns. The source is not safe for concurrent capture calls. You can poll takeFeedback from a different thread.
import ("time""github.com/pion/rtcp""github.com/pion/webrtc/v4""github.com/pion/webrtc/v4/pkg/media"lksdk "github.com/livekit/server-sdk-go/v2")codec := webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264,ClockRate: 90000,}track, err := lksdk.NewLocalSampleTrack(codec,lksdk.WithRTCPHandler(func(packet rtcp.Packet) {switch packet.(type) {case *rtcp.PictureLossIndication, *rtcp.FullIntraRequest:// Ask your encoder to produce a key frame.}}),)if err != nil {return err}_, err = room.LocalParticipant.PublishTrack(track,&lksdk.TrackPublicationOptions{Name: "camera",VideoWidth: 1920,VideoHeight: 1080,},)if err != nil {return err}for {// Read one complete access unit from your encoder.payload := nextAccessUnit()err = track.WriteSample(media.Sample{Data: payload,Duration: time.Second / 30,}, nil)if err != nil {return err}}
WriteSample handles RTP packetization and timestamps. Use WriteRTP instead when your application already produces RTP packets.
Encoder feedback
A subscriber can join after the most recent key frame. Network loss can also make a delta frame impossible to decode. In both cases, the publishing pipeline requests a new key frame.
The source provides key-frame and rate-control feedback. Poll take_keyframe_request and take_rate_control_request during the capture loop and pass each request to the upstream encoder.
The source provides key-frame and rate-control feedback. Poll takeFeedback during the capture loop and pass each request to the upstream encoder.
The server SDK passes RTCP packets to WithRTCPHandler. Handle Picture Loss Indication (PLI) and Full Intra Request (FIR) packets to request a key frame.