Stream a video

Preview
Publish a camera asset and start an RTSP, MPEG-TS or SRT ingress stream.

Publish a camera asset entity, start an ingress stream over RTSP, MPEG-TS or SRT, and attach the stream to the entity so operators can view the feed in the Lattice UI.

Before you begin

  • Set up a Lattice environment with a valid LATTICE_ENDPOINT, LATTICE_CLIENT_ID, LATTICE_CLIENT_SECRET, and SANDBOXES_TOKEN. See Lattice Sandboxes for details on obtaining credentials.
  • Get the Lattice gRPC SDK if you’re running the gRPC examples.
gRPC authentication

If you are using gRPC with client credentials, set up the token refresh module before running the examples on this page.

Start a stream

Lattice supports the following video streaming protocols:

If you don’t already have an RTSP source, stand one up with MediaMTX:

1

Install MediaMTX

Download a pre-built MediaMTX binary for your platform from the MediaMTX releases page and place it on the host that publishes the camera feed.

2

Configure the RTSP listener

In your mediamtx.yml, set the RTSP listener address and define a /cam or similar path that reads from the local camera. Set source to match your camera; see the MediaMTX paths documentation for the available source types.

mediamtx.yml
1rtspAddress: :<your-port>
2
3paths:
4 cam:
5 source: <your-camera-source>
3

Start MediaMTX

Start the server in the foreground to verify connectivity, then move it to a systemd service for production use.

$./mediamtx mediamtx.yml

You see [RTSP] listener opened on :<your-port> once the server is ready.

4

Verify the stream

From any host on the same network, confirm that the feed is playable. Replace <rtsp-host> with the address of the host running MediaMTX.

$ffplay rtsp://<rtsp-host>:<your-port>/cam

A playback window confirms the source is ready for Lattice ingestion.

Before attaching a video to an asset, the asset must exist as a Lattice entity. Publish a camera entity with the TEMPLATE_ASSET ontology and a PlatformType of CAMERA, then refresh it on a heartbeat so it does not expire:

1

Publish a camera

Construct an Entity with the components Lattice needs to render a friendly land-based camera asset on the common operational picture (COP). ExpiryTime is set five minutes out and paired with a five-second publish loop, so the entity stays alive as long as the publisher runs.

1// This Go example uses the Lattice REST SDK (lattice-sdk-go) to publish a camera asset entity.
2// Attaching a video to an asset requires the asset to already exist as a Lattice entity, so
3// publish the camera on a heartbeat before starting an ingress stream.
4package main
5
6import (
7 "context"
8 "fmt"
9 "net/http"
10 "os"
11 "time"
12
13 Lattice "github.com/anduril/lattice-sdk-go/v5"
14 "github.com/anduril/lattice-sdk-go/v5/client"
15 "github.com/anduril/lattice-sdk-go/v5/option"
16 "github.com/google/uuid"
17)
18
19const (
20 cameraName = "camera-01"
21 integrationName = "lattice_video_integration"
22 dataType = "camera_video_feed"
23 publishInterval = 5 * time.Second
24 entityExpiryTtlSecs = 60
25)
26
27func main() {
28 // Get environment variables
29 latticeEndpoint := os.Getenv("LATTICE_ENDPOINT")
30 clientSecret := os.Getenv("LATTICE_CLIENT_SECRET")
31 clientId := os.Getenv("LATTICE_CLIENT_ID")
32
33 // Remove sandboxesToken from the following statements if you are not developing on Sandboxes.
34 sandboxesToken := os.Getenv("SANDBOXES_TOKEN")
35
36 // Check required environment variables
37 if latticeEndpoint == "" || clientId == "" || clientSecret == "" || sandboxesToken == "" {
38 fmt.Println("Missing required environment variables")
39 os.Exit(1)
40 }
41
42 // Initialize headers for sandbox authorization
43 headers := http.Header{}
44 headers.Add("Anduril-Sandbox-Authorization", fmt.Sprintf("Bearer %s", sandboxesToken))
45
46 // Create the client
47 LatticeClient := client.NewClient(
48 option.WithClientCredentials(clientId, clientSecret),
49 option.WithBaseURL(fmt.Sprintf("https://%s", latticeEndpoint)),
50 option.WithHTTPHeader(headers),
51 )
52
53 // Stable entity ID derived from the camera's hardware identifier so the same
54 // physical camera maps to the same Lattice entity across restarts.
55 entityId := uuid.NewSHA1(uuid.NameSpaceURL, []byte(cameraName)).String()
56
57 creationTime := time.Now().UTC()
58
59 // Continuously publish the entity so it does not expire.
60 for {
61 latestTimestamp := time.Now().UTC()
62 ctx := context.Background()
63
64 entity := Lattice.Entity{
65 EntityID: &entityId,
66 Description: Lattice.String("EO camera streaming live video"),
67 Aliases: &Lattice.Aliases{
68 Name: Lattice.String("Camera 01"),
69 AlternateIDs: []*Lattice.AlternateID{
70 {
71 ID: Lattice.String(cameraName),
72 Type: Lattice.AlternateIDTypeAltIDTypeSerialNumber.Ptr(),
73 },
74 },
75 },
76 IsLive: Lattice.Bool(true),
77 CreatedTime: Lattice.Time(creationTime),
78 // ExpiryTime is set in the future and paired with a shorter publish loop,
79 // so the entity stays alive as long as the publisher runs.
80 ExpiryTime: Lattice.Time(latestTimestamp.Add(entityExpiryTtlSecs * time.Second)),
81 Ontology: &Lattice.Ontology{
82 Template: Lattice.OntologyTemplateTemplateAsset.Ptr(),
83 PlatformType: Lattice.String("CAMERA"),
84 },
85 MilView: &Lattice.MilView{
86 Disposition: Lattice.MilViewDispositionDispositionFriendly.Ptr(),
87 Environment: Lattice.MilViewEnvironmentEnvironmentLand.Ptr(),
88 Nationality: Lattice.MilViewNationalityNationalityUnitedStatesOfAmerica.Ptr(),
89 },
90 // Overall classification of the entity's data.
91 DataClassification: &Lattice.Classification{
92 Default: &Lattice.ClassificationInformation{
93 Level: Lattice.ClassificationInformationLevelClassificationLevelsUnclassified.Ptr(),
94 },
95 },
96 Location: &Lattice.Location{
97 Position: &Lattice.Position{
98 LatitudeDegrees: Lattice.Float64(36.66633492530301),
99 LongitudeDegrees: Lattice.Float64(-116.9117674522639),
100 AltitudeHaeMeters: Lattice.Float64(780.0),
101 },
102 },
103 Provenance: &Lattice.Provenance{
104 IntegrationName: Lattice.String(integrationName),
105 DataType: Lattice.String(dataType),
106 SourceUpdateTime: Lattice.Time(latestTimestamp),
107 },
108 Health: &Lattice.Health{
109 ConnectionStatus: Lattice.HealthConnectionStatusConnectionStatusOnline.Ptr(),
110 HealthStatus: Lattice.HealthHealthStatusHealthStatusHealthy.Ptr(),
111 ActiveAlerts: []*Lattice.Alert{},
112 UpdateTime: Lattice.Time(latestTimestamp),
113 // Per-subsystem health rolls up into the entity's overall
114 // health_status; report at least the camera sensor itself.
115 Components: []*Lattice.ComponentHealth{
116 {
117 ID: Lattice.String("camera-01-eo"),
118 Name: Lattice.String("EO camera sensor"),
119 Health: Lattice.ComponentHealthHealthHealthStatusHealthy.Ptr(),
120 UpdateTime: Lattice.Time(latestTimestamp),
121 },
122 },
123 },
124 // Declare the camera as a sensor so Lattice can render the sensor cone.
125 Sensors: &Lattice.Sensors{
126 Sensors: []*Lattice.Sensor{
127 {
128 SensorID: Lattice.String("camera-01-eo"),
129 SensorType: Lattice.SensorSensorTypeSensorTypeCamera.Ptr(),
130 OperationalState: Lattice.SensorOperationalStateOperationalStateOperational.Ptr(),
131 SensorDescription: Lattice.String("EO camera, H.264"),
132 FieldsOfView: []*Lattice.FieldOfView{
133 {
134 CenterRayPose: &Lattice.EntityManagerPose{
135 Orientation: &Lattice.Quaternion{
136 X: Lattice.Float64(0.0),
137 Y: Lattice.Float64(0.0),
138 Z: Lattice.Float64(0.0),
139 W: Lattice.Float64(1.0),
140 },
141 },
142 HorizontalFov: Lattice.Float64(1.204), // ~69 deg (typical EO horizontal FoV)
143 VerticalFov: Lattice.Float64(0.954), // ~54.6 deg (typical EO vertical FoV)
144 Range: Lattice.Float64(150.0),
145 Mode: Lattice.FieldOfViewModeSensorModeSearch.Ptr(),
146 },
147 },
148 },
149 },
150 },
151 TaskCatalog: &Lattice.TaskCatalog{
152 TaskDefinitions: []*Lattice.TaskDefinition{
153 {TaskSpecificationURL: Lattice.String("type.googleapis.com/<org>.<package>.<version>.<task>")},
154 },
155 },
156 }
157
158 // Publish the entity
159 _, err := LatticeClient.Entities.PublishEntity(ctx, &entity)
160 if err != nil {
161 fmt.Printf("Error publishing entity: %v\n", err)
162 } else {
163 fmt.Printf("Publishing asset with entity ID: %s\n", entityId)
164 }
165
166 time.Sleep(publishInterval)
167 }
168}

The heartbeat loop is the simplest pattern for keeping a transient asset alive. For long-running deployments, consider publishing from a supervised service that resets ExpiryTime based on hardware health rather than a fixed interval.

2

Set the stream endpoint

The following generates a client-side UUID for ingress_id, calls CreateIngressStream with your RTSP stream URL, then attaches the new video to the camera entity using OverrideEntity:

1// This Go example uses the Lattice REST SDK (lattice-sdk-go) to start an RTSP ingress stream and
2// attach it to a camera asset entity.
3package main
4
5import (
6 "context"
7 "fmt"
8 "net/http"
9 "os"
10 "time"
11
12 Lattice "github.com/anduril/lattice-sdk-go/v5"
13 "github.com/anduril/lattice-sdk-go/v5/client"
14 "github.com/anduril/lattice-sdk-go/v5/option"
15 "github.com/google/uuid"
16)
17
18const (
19 // Entity ID to associate the stream with. Replace before running.
20 entityID = "<ENTITY-ID>"
21 // RTSP source Lattice will pull from.
22 rtspURL = "rtsp://<rtsp-host>:<port>/cam"
23)
24
25func main() {
26 latticeEndpoint := os.Getenv("LATTICE_ENDPOINT")
27 clientSecret := os.Getenv("LATTICE_CLIENT_SECRET")
28 clientId := os.Getenv("LATTICE_CLIENT_ID")
29
30 // Remove sandboxesToken from the following statements if you are not developing on Sandboxes.
31 sandboxesToken := os.Getenv("SANDBOXES_TOKEN")
32
33 if latticeEndpoint == "" || clientId == "" || clientSecret == "" || sandboxesToken == "" {
34 fmt.Println("Missing required environment variables")
35 os.Exit(1)
36 }
37
38 headers := http.Header{}
39 headers.Add("Anduril-Sandbox-Authorization", fmt.Sprintf("Bearer %s", sandboxesToken))
40
41 LatticeClient := client.NewClient(
42 option.WithClientCredentials(clientId, clientSecret),
43 option.WithBaseURL(fmt.Sprintf("https://%s", latticeEndpoint)),
44 option.WithHTTPHeader(headers),
45 )
46
47 ctx := context.Background()
48
49 // Generate a client-side UUID so the stream is correlatable and the call is retry-safe.
50 ingressID := uuid.New().String()
51 fmt.Printf("Starting RTSP ingress stream %s from %s\n", ingressID, rtspURL)
52
53 // RTSP is a pull protocol: Lattice dials the supplied URL, so no push endpoint
54 // is returned.
55 createResp, err := LatticeClient.Video.CreateIngressStream(ctx, &Lattice.CreateIngressStreamRequest{
56 IngressID: &ingressID,
57 Title: Lattice.String("cam-01"),
58 Rtsp: &Lattice.RtspSettings{
59 URL: Lattice.String(rtspURL),
60 },
61 })
62 if err != nil {
63 fmt.Printf("Failed to create ingress stream: %v\n", err)
64 os.Exit(1)
65 }
66 fmt.Printf("Successfully started stream with ID: %s\n", *createResp.GetIngressID())
67
68 // Attach the new video to the asset entity. A field_path of media.media replaces
69 // only the media list, leaving every other component on the entity untouched.
70 _, err = LatticeClient.Entities.OverrideEntity(ctx, &Lattice.EntityOverride{
71 EntityID: entityID,
72 FieldPath: "media.media",
73 Entity: &Lattice.Entity{
74 EntityID: Lattice.String(entityID),
75 Media: &Lattice.Media{
76 Media: []*Lattice.MediaItem{
77 {
78 ItemIdentifier: &ingressID,
79 Type: Lattice.MediaItemTypeMediaTypeVideo.Ptr(),
80 },
81 },
82 },
83 },
84 Provenance: &Lattice.Provenance{
85 IntegrationName: Lattice.String("lattice-video-integration"),
86 DataType: Lattice.String("video-association"),
87 SourceUpdateTime: Lattice.Time(time.Now().UTC()),
88 },
89 })
90 if err != nil {
91 fmt.Printf("Failed to override entity media: %v\n", err)
92 os.Exit(1)
93 }
94 fmt.Printf("Associated video %s with entity %s\n", ingressID, entityID)
95}

Once packets start arriving, operators select the camera asset in Lattice to view the live feed.

Shows the live camera feed playing for the selected asset in Lattice.

What’s next