Repository: viam-modules/viamrtsp
Pull request #112: [RSDK-10002] - make video store viamrtsp model
Author: login: nicksanford
Created: 2025-03-10T17:42:07Z | Merged: 2025-03-11T22:05:52Z

[Ticket](https://viam.atlassian.net/browse/RSDK-10002)

Depends on: https://github.com/viam-modules/video-store/pull/54

Changes:
1. Add `viam:viamrtsp:video-store` model as a generic component which implements the same `DoCommand` the video-store model in the video-store module does. That resource doesn't take a `sync` config param. Config is defined here: https://github.com/viam-modules/viamrtsp/pull/112/files#diff-b151fd77787feb836a509df656993f17d30f3ae6300dc39d96c7b6e8098403a4R12. It does allow the camera to be unspecified and if so it will still boot and allow the user to query video via `DoCommand(fetch)` and `DoCommand(save)`.
2. If the camera is specified but doesn't support the codecs video-store supports (h264 and h265 in this PR) it will return an error from the video-store contstructor and resource graph will retry.
3. If the camera started off using a supported codec and then it changed (either via reconfiguration or b/c the IP camera started sending video in a different codec) video-store will emit a log ever 5 seconds stating it is waiting for video data until it gets video data. This should be an infrequent condition.
5. Delete the embedded video-store and make DoCommand unimplemented
6. Create global registry in registry.go to allow viamrtsp cameras to register themselves for video-stores to find, and for viamrtsp cameras to send video to the video-stores that request it.
7. Add a Mux struct that remembers the sps, pps, and for h265 vps during the video stream as well as decodes dts and normalizes dts and pts to 0 before passing it to the raw segmenter. This allows video-store to start collecting video mid rtsp stream and prevents video-store from needing to force viamrtsp to restart the video in order to produce uncorrepted video segment files.
8. Add videoRequest struct to manage the lifecycle of the videostore's request to viamrtsp for video.
 
Deps changes:
1. Upgrade RDK from v0.57.1-0.20250108163155-2071720cd8d3 to v0.65.0
10. Upgrade goutils from v0.1.118 to v0.1.130
11. Upgrade viam api from v0.1.380  to v0.1.396
12. Upgrade video-store from v0.0.5-rc2 -  v0.0.5-rc4


```json
{
  "agent": {
    "version_control": {
      "viam-server": "0.65.0"
    }
  },
  "components": [
    {
      "name": "viamrtsp-video-store",
      "api": "rdk:component:generic",
      "model": "viam:viamrtsp:video-store",
      "attributes": {
        "camera": "cam",
        "storage": {
          "size_gb": 1
        }
      }
    },
    {
      "name": "cam",
      "api": "rdk:component:camera",
      "model": "viam:viamrtsp:rtsp",
      "attributes": {
        "rtsp_address": "...",
        "lazy_decode": true
      }
    }
  ],
  "modules": [
    {
      "type": "local",
      "name": "viamrtsp-module-1",
      "executable_path": "..."
    }
  ]
}
```

Nick Manual Testing:
1. Changing `storage.size_gb`, `storage.upload_path` and `storage.storage_path`
2. Removing `camera` and confirming that video can still be queried
3. Configuring `camera` to a non viamrtsp camera (logs errors until fixed)
4. Configuring `camera` to a non existant resource name
5. Configuring `camera` to a resource that is not a camera
6. Disabling viamrtsp camera videostore is depending on (logs errors until fixed)
7. Disabling video-store while viamrtsp is receiving video from it
8. Configuring 2 video stores to consume video from the same viamrtsp camera (doesn't work for second video-store and logs errors until fixed)
9. Changing the IP camera viamrtsp is consuming from from a video-store supported codec (such has h264 or h265) to an unsupported codec (such as mpeg4), logs errors until fixed
10. Starting videostore when viamrtsp is using an unsupported codec (logs errors until fixed)
11. Configuring multiple video-stores and viamrtsp cameras


Diff:
diff --git a/cmd/module/cmd.go b/cmd/module/cmd.go
index 0e212139..32c91658 100644
--- a/cmd/module/cmd.go
+++ b/cmd/module/cmd.go
@@ -7,7 +7,9 @@ import (
 	"github.com/viam-modules/viamrtsp"
 	"github.com/viam-modules/viamrtsp/upnpdiscovery"
 	"github.com/viam-modules/viamrtsp/viamonvif"
+	"github.com/viam-modules/viamrtsp/videostore"
 	"go.viam.com/rdk/components/camera"
+	"go.viam.com/rdk/components/generic"
 	"go.viam.com/rdk/module"
 	"go.viam.com/rdk/services/discovery"
 )
@@ -31,6 +33,12 @@ func realMain(ctx context.Context) error {
 			return err
 		}
 	}
+
+	err = myMod.AddModelFromRegistry(ctx, generic.API, videostore.Model)
+	if err != nil {
+		return err
+	}
+
 	err = myMod.AddModelFromRegistry(ctx, discovery.API, viamonvif.Model)
 	if err != nil {
 		return err
diff --git a/cmd/remote/cmd.go b/cmd/remote/cmd.go
index b5bd3e82..c01e8def 100644
--- a/cmd/remote/cmd.go
+++ b/cmd/remote/cmd.go
@@ -46,7 +46,7 @@ func mainWithArgs(ctx context.Context, _ []string, logger logging.Logger) error
 		},
 	}
 
-	myRobot, err := robotimpl.New(ctx, conf, logger)
+	myRobot, err := robotimpl.New(ctx, conf, nil, logger)
 	if err != nil {
 		return err
 	}
diff --git a/go.mod b/go.mod
index 3867eed0..83dbaeda 100644
--- a/go.mod
+++ b/go.mod
@@ -18,11 +18,11 @@ require (
 	github.com/pion/rtp v1.8.11
 	github.com/rhysd/actionlint v1.6.27
 	github.com/stretchr/testify v1.10.0
-	github.com/viam-modules/video-store v0.0.5-rc2
+	github.com/viam-modules/video-store v0.0.5-rc4
 	go.uber.org/zap v1.27.0
-	go.viam.com/rdk v0.57.1-0.20250108163155-2071720cd8d3
+	go.viam.com/rdk v0.65.0
 	go.viam.com/test v1.2.4
-	go.viam.com/utils v0.1.118
+	go.viam.com/utils v0.1.130
 	golang.org/x/net v0.34.0
 )
 
@@ -308,7 +308,7 @@ require (
 	go.uber.org/automaxprocs v1.5.3 // indirect
 	go.uber.org/goleak v1.3.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	go.viam.com/api v0.1.380 // indirect
+	go.viam.com/api v0.1.396 // indirect
 	goji.io v2.0.2+incompatible // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
 	golang.org/x/crypto v0.32.0 // indirect
@@ -331,6 +331,7 @@ require (
 	google.golang.org/grpc v1.66.0 // indirect
 	google.golang.org/protobuf v1.35.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 4f48a3bc..1ff08c80 100644
--- a/go.sum
+++ b/go.sum
@@ -1225,8 +1225,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
 github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
 github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
-github.com/viam-modules/video-store v0.0.5-rc2 h1:f1aCP5/2+pLz76wdtLNziF0SEFn9gubqXQUeIOtIhJs=
-github.com/viam-modules/video-store v0.0.5-rc2/go.mod h1:+ywAnBUIOeLNBHjqUMefRP2MqMNs7LfWtyh+ifdAQCY=
+github.com/viam-modules/video-store v0.0.5-rc4 h1:lmv8CE2pD8xXxpfpLEgTBScCMdFhxKXu+oTIWy+o884=
+github.com/viam-modules/video-store v0.0.5-rc4/go.mod h1:gM2ApIceni3LazLub9srwr75rJFcF3Hcy2FsZDXTW8E=
 github.com/viamrobotics/evdev v0.1.3 h1:mR4HFafvbc5Wx4Vp1AUJp6/aITfVx9AKyXWx+rWjpfc=
 github.com/viamrobotics/evdev v0.1.3/go.mod h1:N6nuZmPz7HEIpM7esNWwLxbYzqWqLSZkfI/1Sccckqk=
 github.com/viamrobotics/webrtc/v3 v3.99.10 h1:ykE14wm+HkqMD5Ozq4rvhzzfvnXAu14ak/HzA1OCzfY=
@@ -1327,14 +1327,14 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.viam.com/api v0.1.380 h1:VgRHDlPBku+kjIp4omxmPNmRVZezytFUUOFJ2xpRFR8=
-go.viam.com/api v0.1.380/go.mod h1:g5eipXHNm0rQmW7DWya6avKcmzoypLmxnMlAaIsE5Ls=
-go.viam.com/rdk v0.57.1-0.20250108163155-2071720cd8d3 h1:NEeDXwj+ronqenpfRPPXCMv8cf8IaendAfY/2pBwOXw=
-go.viam.com/rdk v0.57.1-0.20250108163155-2071720cd8d3/go.mod h1:dbyaFAvkw1gU8PZYlBENUaHis9us8hnKFSU2QBctAZ4=
+go.viam.com/api v0.1.396 h1:HqX+zt24BJFPcIQZk1fttDR6goiRCTt1pcVZyBOi7n4=
+go.viam.com/api v0.1.396/go.mod h1:drvlBWaiHFxPziz5jayHvibez1qG7lylcNCC1LF8onU=
+go.viam.com/rdk v0.65.0 h1:AoUjTjgn+OPKDIxs0eydZ8CoKfycwc1WRK3ACWQTiEg=
+go.viam.com/rdk v0.65.0/go.mod h1:SUaX+NIgrYTo9kXRN/5hMF+cPNUAKBcS+U2PXa99R4o=
 go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug=
 go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI=
-go.viam.com/utils v0.1.118 h1:Kp6ebrCBiYReeSC1XnWPTjtBJoTUsQ6YWAomQkQF/mE=
-go.viam.com/utils v0.1.118/go.mod h1:g1CaEkf7aJCrSI/Sfkx+6cBS1+Y3fPT2pvMQbp7TTBI=
+go.viam.com/utils v0.1.130 h1:5vggHNK/ar/YHXjf9z5WuihX+n9jYFnA2CEFOYi99d8=
+go.viam.com/utils v0.1.130/go.mod h1:g1CaEkf7aJCrSI/Sfkx+6cBS1+Y3fPT2pvMQbp7TTBI=
 go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
 go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
 goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
@@ -1843,6 +1843,8 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
 gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
diff --git a/registry/registry.go b/registry/registry.go
new file mode 100644
index 00000000..92ffd6e6
--- /dev/null
+++ b/registry/registry.go
@@ -0,0 +1,80 @@
+// Package registry provides a means for videostores to find viamrtsp cameras within the same OS process
+package registry
+
+import (
+	"context"
+	"errors"
+	"sync"
+
+	"github.com/viam-modules/video-store/videostore"
+)
+
+// Mux is how viamrtsp writes it's video data to a video-store that has requested it.
+type Mux interface {
+	// Start starts saving an rtsp stream's video
+	Start(codec videostore.CodecType, initialParameters [][]byte) error
+	// WritePacket writes a packet
+	WritePacket(codec videostore.CodecType, au [][]byte, pts int64) error
+	// Stop stops the mux so any resources taken during Start can be released
+	Stop() error
+}
+
+// ModuleCamera allows videostore to request video from a camera and cancel that request.
+type ModuleCamera interface {
+	// RequestVideo requests the camera write video to the mux if the camera supports one of the codecs
+	RequestVideo(vs Mux, codecCandiates []videostore.CodecType) (context.Context, error)
+	// CancelRequest cancels a request
+	CancelRequest(vs Mux) error
+}
+
+// ModuleRegistry allows ModuleCamera s to be added, removed and queried from the global registry.
+type ModuleRegistry struct {
+	mu   sync.Mutex
+	cams map[string]ModuleCamera
+}
+
+var (
+	// ErrAlreadyInRegistry means that the resource is already in the registry.
+	ErrAlreadyInRegistry = errors.New("already in registry")
+	// ErrNotFound means that the resource was not found.
+	ErrNotFound = errors.New("resource not found")
+	// ErrUnsupported means that the codec is unsupported.
+	ErrUnsupported = errors.New("unsupported codec")
+	// ErrBusy means that the resource is busy.
+	ErrBusy = errors.New("busy")
+	// Global is the global registry.
+	Global = &ModuleRegistry{cams: map[string]ModuleCamera{}}
+)
+
+// Get returns the ModuleCamera with the name from the global registry.
+func (mr *ModuleRegistry) Get(name string) (ModuleCamera, error) {
+	mr.mu.Lock()
+	defer mr.mu.Unlock()
+	cam, ok := mr.cams[name]
+	if !ok {
+		return nil, ErrNotFound
+	}
+	return cam, nil
+}
+
+// Add adds a ModuleCamera with a name to the global registry.
+func (mr *ModuleRegistry) Add(name string, val ModuleCamera) error {
+	mr.mu.Lock()
+	defer mr.mu.Unlock()
+	if _, ok := mr.cams[name]; ok {
+		return ErrAlreadyInRegistry
+	}
+	mr.cams[name] = val
+	return nil
+}
+
+// Remove removes a ModuleCamera with a name from the global registry.
+func (mr *ModuleRegistry) Remove(name string) error {
+	mr.mu.Lock()
+	defer mr.mu.Unlock()
+	if _, ok := mr.cams[name]; !ok {
+		return ErrNotFound
+	}
+	delete(mr.cams, name)
+	return nil
+}
diff --git a/rtsp.go b/rtsp.go
index 42380bad..b3e190ed 100644
--- a/rtsp.go
+++ b/rtsp.go
@@ -3,12 +3,10 @@ package viamrtsp
 
 import (
 	"context"
-	"encoding/base64"
 	"encoding/hex"
 	"errors"
 	"fmt"
-	"os"
-	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -27,6 +25,7 @@ import (
 	"github.com/erh/viamupnp"
 	"github.com/pion/rtp"
 	"github.com/viam-modules/viamrtsp/formatprocessor"
+	"github.com/viam-modules/viamrtsp/registry"
 	"github.com/viam-modules/video-store/videostore"
 	"go.uber.org/zap/zapcore"
 	"go.viam.com/rdk/components/camera"
@@ -54,11 +53,6 @@ const (
 	// defaultMPEG4ProfileLevelID is the default profile-level-id value for MPEG4 video
 	// as specified in RFC 6416 Section 7.1 https://datatracker.ietf.org/doc/html/rfc6416#section-7.1
 	defaultMPEG4ProfileLevelID = 1
-	defaultSegmentSeconds      = 30 // seconds
-	defaultUploadPath          = ".viam/capture/video-upload"
-	defaultStoragePath         = ".viam/video-storage-viamrtsp"
-	maxGRPCSize                = 1024 * 1024 * 32 // bytes
-	videoStoreInitCloseTimeout = time.Second * 10
 )
 
 var (
@@ -123,21 +117,6 @@ func (conf *Config) Validate(path string) ([]string, error) {
 		return nil, fmt.Errorf("invalid address '%s' for component at path '%s': %w", conf.Address, path, err)
 	}
 
-	if conf.VideoStore != nil {
-		logging.Global().Infof("VideoStore: %#v", *conf.VideoStore)
-		sc, err := applyStorageDefaults(conf.VideoStore.Storage, "anyname")
-		if err != nil {
-			return nil, err
-		}
-		vsc := videostore.Config{
-			Type:    videostore.SourceTypeH264RTPPacket,
-			Storage: sc,
-		}
-		if err := vsc.Validate(); err != nil {
-			return nil, err
-		}
-	}
-
 	var deps []string
 	if conf.DiscoveryDep != "" {
 		deps = []string{conf.DiscoveryDep}
@@ -193,13 +172,12 @@ type rtspCamera struct {
 	lazyDecode       bool
 	iframeOnlyDecode bool
 
-	closeMu         sync.RWMutex
-	vsConfig        *videostore.Config
-	videoStoreMuxer *videoStoreMuxer
-	auMu            sync.Mutex
-	au              [][]byte
-	client          *gortsplib.Client
-	rawDecoder      *decoder
+	closeMu      sync.RWMutex
+	videoRequest *videoRequest
+	auMu         sync.Mutex
+	au           [][]byte
+	client       *gortsplib.Client
+	rawDecoder   *decoder
 
 	cancelCtx  context.Context
 	cancelFunc context.CancelFunc
@@ -237,9 +215,11 @@ type rtspCamera struct {
 
 // Close closes the camera. It always returns nil, but because of Close() interface, it needs to return an error.
 func (rc *rtspCamera) Close(_ context.Context) error {
+	if err := registry.Global.Remove(rc.Name().String()); err != nil {
+		rc.logger.Errorf("error removing camera from global registry: %s", err.Error())
+	}
 	rc.cancelFunc()
 	rc.closeMu.Lock()
-	defer rc.closeMu.Unlock()
 	rc.unsubscribeAll()
 	rc.activeBackgroundWorkers.Wait()
 	rc.closeConnection()
@@ -255,6 +235,8 @@ func (rc *rtspCamera) Close(_ context.Context) error {
 	}
 	rc.latestFrameMu.Unlock()
 	rc.avFramePool.close()
+	rc.closeMu.Unlock()
+	rc.videoRequest.clear()
 	return nil
 }
 
@@ -306,11 +288,7 @@ func (rc *rtspCamera) closeConnection() {
 		rc.rawDecoder.close()
 		rc.rawDecoder = nil
 	}
-
-	if rc.videoStoreMuxer != nil {
-		rc.videoStoreMuxer.CloseVideoStore()
-		rc.videoStoreMuxer = nil
-	}
+	rc.videoRequest.stop()
 }
 
 // reconnectClientWithFallbackTransports attempts to setup the RTSP client with the given codec
@@ -506,22 +484,6 @@ func (rc *rtspCamera) initH264(session *description.Session) (err error) {
 		return fmt.Errorf("creating H264 raw decoder: %w", err)
 	}
 
-	if rc.vsConfig != nil {
-		config := *rc.vsConfig
-		config.Type = videostore.SourceTypeH264RTPPacket
-		vs, err := videostore.NewRTPVideoStore(config, rc.logger)
-		if err != nil {
-			return err
-		}
-		rc.videoStoreMuxer = &videoStoreMuxer{
-			Config: config,
-			vs:     vs,
-			sps:    f.SPS,
-			pps:    f.PPS,
-			logger: rc.logger,
-		}
-	}
-
 	// if SPS and PPS are present into the SDP, send them to the decoder
 	initialSPSAndPPS := [][]byte{}
 	if f.SPS != nil {
@@ -593,6 +555,7 @@ func (rc *rtspCamera) initH264(session *description.Session) (err error) {
 		}
 	}
 
+	params := [][]byte{f.SPS, f.PPS}
 	onPacketRTP := func(pkt *rtp.Packet) {
 		pts, ok := rc.client.PacketPTS2(media, pkt)
 		if !ok {
@@ -610,7 +573,7 @@ func (rc *rtspCamera) initH264(session *description.Session) (err error) {
 			return
 		}
 		storeImage(au)
-		rc.videoStoreMuxer.writeH264(au, pts)
+		rc.videoRequest.write(videostore.CodecTypeH264, params, au, pts)
 	}
 
 	_, err = rc.client.Setup(session.BaseURL, media, 0, 0)
@@ -623,6 +586,23 @@ func (rc *rtspCamera) initH264(session *description.Session) (err error) {
 	return nil
 }
 
+var codecToCodecType = map[videoCodec]videostore.CodecType{
+	H264: videostore.CodecTypeH264,
+	H265: videostore.CodecTypeH264,
+}
+
+func (rc *rtspCamera) RequestVideo(mux registry.Mux, codecCandiates []videostore.CodecType) (context.Context, error) {
+	currentCodec := codecToCodecType[videoCodec(rc.currentCodec.Load())]
+	if !slices.Contains(codecCandiates, currentCodec) {
+		return nil, registry.ErrUnsupported
+	}
+	return rc.videoRequest.newRequest(mux)
+}
+
+func (rc *rtspCamera) CancelRequest(mux registry.Mux) error {
+	return rc.videoRequest.cancelRequest(mux)
+}
+
 // initH265 initializes the H265 decoder and sets up the client to receive H265 packets.
 func (rc *rtspCamera) initH265(session *description.Session) (err error) {
 	if rc.rtpPassthrough {
@@ -674,23 +654,6 @@ func (rc *rtspCamera) initH265(session *description.Session) (err error) {
 		rc.logger.Warn("no PPS found in H265 format")
 	}
 
-	if rc.vsConfig != nil {
-		config := *rc.vsConfig
-		config.Type = videostore.SourceTypeH265RTPPacket
-		vs, err := videostore.NewRTPVideoStore(config, rc.logger)
-		if err != nil {
-			return err
-		}
-		rc.videoStoreMuxer = &videoStoreMuxer{
-			Config: config,
-			vs:     vs,
-			vps:    f.VPS,
-			sps:    f.SPS,
-			pps:    f.PPS,
-			logger: rc.logger,
-		}
-	}
-
 	storeImage := func(au [][]byte) {
 		if rc.iframeOnlyDecode && !h265.IsRandomAccess(au) {
 			return
@@ -707,6 +670,7 @@ func (rc *rtspCamera) initH265(session *description.Session) (err error) {
 		}
 	}
 
+	params := [][]byte{f.VPS, f.SPS, f.PPS}
 	onPacketRTP := func(pkt *rtp.Packet) {
 		// we need to do this before calling Decode as apparently rtpDec.Decode mutates
 		// the packet such that one can't get the pts out after Decode is called
@@ -725,7 +689,7 @@ func (rc *rtspCamera) initH265(session *description.Session) (err error) {
 			return
 		}
 		storeImage(au)
-		rc.videoStoreMuxer.writeH265(au, pts)
+		rc.videoRequest.write(videostore.CodecTypeH265, params, au, pts)
 	}
 
 	_, err = rc.client.Setup(session.BaseURL, media, 0, 0)
@@ -791,9 +755,9 @@ func (rc *rtspCamera) initMJPEG(session *description.Session) error {
 		rc.logger.Warn("i_frame_only_decode is currently only supported for H264 and H265 codecs. " +
 			"lazy_decode features disabled due to MJPEG RTSP track")
 	}
-
-	if rc.vsConfig != nil {
-		rc.logger.Warn("video_store is only supported for H264 codec. video_store features disabled due to MJPEG RTSP track")
+	if rc.videoRequest.active() {
+		rc.logger.Warn("video-store is currently only supported for H264 and H265 codecs. " +
+			"unable to store video due to MJPEG RTSP track")
 	}
 
 	var f *format.MJPEG
@@ -889,8 +853,9 @@ func (rc *rtspCamera) initMPEG4(session *description.Session) error {
 			"lazy_decode features disabled due to MPEG4 RTSP track")
 	}
 
-	if rc.vsConfig != nil {
-		rc.logger.Warn("video_store is only supported for H264 codec. video_store features disabled due to MPEG4 RTSP track")
+	if rc.videoRequest.active() {
+		rc.logger.Warn("video-store is currently only supported for H264 and H265 codecs. " +
+			"unable to store video due to MPEG4 RTSP track")
 	}
 
 	var f *format.MPEG4Video
@@ -1116,23 +1081,6 @@ func NewRTSPCamera(ctx context.Context, deps resource.Dependencies, conf resourc
 		rtpPassthrough = *newConf.RTPPassthrough
 	}
 
-	var vsc *videostore.Config
-	if newConf.VideoStore != nil {
-		sc, err := applyStorageDefaults(newConf.VideoStore.Storage, conf.ResourceName().Name)
-		if err != nil {
-			return nil, err
-		}
-		tmpVsc := videostore.Config{
-			Type:    videostore.SourceTypeH264RTPPacket,
-			Storage: sc,
-		}
-		if err := tmpVsc.Validate(); err != nil {
-			return nil, err
-		}
-
-		vsc = &tmpVsc
-	}
-
 	rtpPassthroughCtx, rtpPassthroughCancelCauseFn := context.WithCancelCause(context.Background())
 	cancelCtx, cancel := context.WithCancel(context.Background())
 	rc := &rtspCamera{
@@ -1140,9 +1088,9 @@ func NewRTSPCamera(ctx context.Context, deps resource.Dependencies, conf resourc
 		lazyDecode:                  newConf.LazyDecode,
 		iframeOnlyDecode:            newConf.IframeOnlyDecode,
 		u:                           u,
-		vsConfig:                    vsc,
 		name:                        conf.ResourceName(),
 		rtpPassthrough:              rtpPassthrough,
+		videoRequest:                &videoRequest{logger: logger},
 		bufAndCBByID:                make(map[rtppassthrough.SubscriptionID]bufAndCB),
 		rtpPassthroughCtx:           rtpPassthroughCtx,
 		rtpPassthroughCancelCauseFn: rtpPassthroughCancelCauseFn,
@@ -1159,6 +1107,16 @@ func NewRTSPCamera(ctx context.Context, deps resource.Dependencies, conf resourc
 		return nil, err
 	}
 
+	if err := registry.Global.Add(rc.Name().String(), rc); err != nil {
+		return nil, err
+	}
+	guard := rutils.NewGuard(func() {
+		if err := registry.Global.Remove(rc.Name().String()); err != nil {
+			rc.logger.Errorf("error removing camera: %s", err.Error())
+		}
+	})
+	defer guard.OnFail()
+
 	err = rc.reconnectClientWithFallbackTransports(codecInfo)
 	if err != nil {
 		logger.Error(err.Error())
@@ -1166,44 +1124,10 @@ func NewRTSPCamera(ctx context.Context, deps resource.Dependencies, conf resourc
 	}
 
 	rc.clientReconnectBackgroundWorker(codecInfo)
-
+	guard.Success()
 	return rc, nil
 }
 
-// getHomeDir returns the home directory of the user.
-func getHomeDir() (string, error) {
-	home, err := os.UserHomeDir()
-	if err != nil {
-		return "", err
-	}
-	return home, nil
-}
-
-func applyStorageDefaults(c videoStoreStorageConfig, name string) (videostore.StorageConfig, error) {
-	var zero videostore.StorageConfig
-	if c.UploadPath == "" {
-		home, err := getHomeDir()
-		if err != nil {
-			return zero, err
-		}
-		c.UploadPath = filepath.Join(home, defaultUploadPath, name)
-	}
-	if c.StoragePath == "" {
-		home, err := getHomeDir()
-		if err != nil {
-			return zero, err
-		}
-		c.StoragePath = filepath.Join(home, defaultStoragePath, name)
-	}
-	return videostore.StorageConfig{
-		SegmentSeconds:       defaultSegmentSeconds,
-		SizeGB:               c.SizeGB,
-		OutputFileNamePrefix: name,
-		UploadPath:           c.UploadPath,
-		StoragePath:          c.StoragePath,
-	}, nil
-}
-
 func (rc *rtspCamera) unsubscribeAll() {
 	rc.subsMu.Lock()
 	defer rc.subsMu.Unlock()
@@ -1476,428 +1400,6 @@ func (rc *rtspCamera) NextPointCloud(_ context.Context) (pointcloud.PointCloud,
 	return nil, errors.New("not implemented")
 }
 
-func (rc *rtspCamera) DoCommand(ctx context.Context, command map[string]interface{}) (map[string]interface{}, error) {
-	if rc.videoStoreMuxer == nil {
-		return nil, errors.New("not implemented")
-	}
-	cmd, ok := command["command"].(string)
-	if !ok {
-		return nil, errors.New("invalid command type")
-	}
-
-	switch cmd {
-	// Save command is used to concatenate video clips between the given timestamps.
-	// The concatenated video file is then uploaded to the cloud the upload path.
-	// The response contains the name of the uploaded file.
-	case "save":
-		rc.logger.Debug("save command received")
-		req, err := toSaveCommand(command)
-		if err != nil {
-			return nil, err
-		}
-
-		res, err := rc.videoStoreMuxer.Save(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-
-		ret := map[string]interface{}{
-			"command":  "save",
-			"filename": res.Filename,
-		}
-
-		if req.Async {
-			ret["status"] = "async"
-		}
-		return ret, nil
-	case "fetch":
-		rc.logger.Debug("fetch command received")
-		req, err := toFetchCommand(command)
-		if err != nil {
-			return nil, err
-		}
-		res, err := rc.videoStoreMuxer.Fetch(ctx, req)
-		if err != nil {
-			return nil, err
-		}
-		if len(res.Video) > maxGRPCSize {
-			return nil, errors.New("video file size exceeds max grpc size")
-		}
-		// TODO(seanp): Do we need to encode the video bytes to base64?
-		videoBytesBase64 := base64.StdEncoding.EncodeToString(res.Video)
-		return map[string]interface{}{
-			"command": "fetch",
-			"video":   videoBytesBase64,
-		}, nil
-	default:
-		return nil, errors.New("invalid command")
-	}
-}
-
-func toSaveCommand(command map[string]interface{}) (*videostore.SaveRequest, error) {
-	fromStr, ok := command["from"].(string)
-	if !ok {
-		return nil, errors.New("from timestamp not found")
-	}
-	from, err := videostore.ParseDateTimeString(fromStr)
-	if err != nil {
-		return nil, err
-	}
-	toStr, ok := command["to"].(string)
-	if !ok {
-		return nil, errors.New("to timestamp not found")
-	}
-	to, err := videostore.ParseDateTimeString(toStr)
-	if err != nil {
-		return nil, err
-	}
-	metadata, ok := command["metadata"].(string)
-	if !ok {
-		metadata = ""
-	}
-	async, ok := command["async"].(bool)
-	if !ok {
-		async = false
-	}
-	return &videostore.SaveRequest{
-		From:     from,
-		To:       to,
-		Metadata: metadata,
-		Async:    async,
-	}, nil
-}
-
-func toFetchCommand(command map[string]interface{}) (*videostore.FetchRequest, error) {
-	fromStr, ok := command["from"].(string)
-	if !ok {
-		return nil, errors.New("from timestamp not found")
-	}
-	from, err := videostore.ParseDateTimeString(fromStr)
-	if err != nil {
-		return nil, err
-	}
-	toStr, ok := command["to"].(string)
-	if !ok {
-		return nil, errors.New("to timestamp not found")
-	}
-	to, err := videostore.ParseDateTimeString(toStr)
-	if err != nil {
-		return nil, err
-	}
-	return &videostore.FetchRequest{From: from, To: to}, nil
-}
-
-type extractor interface {
-	Extract(au [][]byte, pts int64) (int64, error)
-}
-
-type videoStoreMuxer struct {
-	videostore.Config
-	width        int
-	height       int
-	vps          []byte
-	sps          []byte
-	pps          []byte
-	dtsExtractor extractor
-	spsUnChanged bool
-	mu           sync.Mutex
-	vs           videostore.RTPVideoStore
-	logger       logging.Logger
-}
-
-// maybeReInitVideoStore assumes mu is held by caller.
-func (e *videoStoreMuxer) maybeReInitVideoStore() error {
-	if e.spsUnChanged {
-		return nil
-	}
-	var width, height int
-	switch e.Config.Type {
-	case videostore.SourceTypeH265RTPPacket:
-		var hsps h265.SPS
-		if err := hsps.Unmarshal(e.sps); err != nil {
-			return err
-		}
-		width, height = hsps.Width(), hsps.Height()
-	case videostore.SourceTypeH264RTPPacket:
-		var hsps h264.SPS
-		if err := hsps.Unmarshal(e.sps); err != nil {
-			return err
-		}
-		width, height = hsps.Width(), hsps.Height()
-	case videostore.SourceTypeFrame:
-		fallthrough
-	case videostore.SourceTypeUnknown:
-		fallthrough
-	default:
-		return errors.New("invalid videostore.SourceType")
-	}
-
-	if width <= 0 || height <= 0 {
-		return errors.New("width and height must both be greater than 0")
-	}
-	// if vs is initialized and the height & width have not changed,
-	// record the sps as unchanged and return
-	if e.vs != nil && e.width == width && e.height == height {
-		e.spsUnChanged = true
-		return nil
-	}
-
-	// if initialized and the height & width have changed,
-	// close and nil out the videostore
-	if e.vs != nil {
-		e.vs.Close()
-		e.vs = nil
-	}
-
-	// if we don't have a video-store we should attempt to initialize one
-	vs, err := videostore.NewRTPVideoStore(e.Config, e.logger)
-	if err != nil {
-		return err
-	}
-
-	if err := vs.Init(width, height); err != nil {
-		return err
-	}
-
-	e.width = width
-	e.height = height
-	e.vs = vs
-	e.spsUnChanged = true
-	return nil
-}
-
-func (e *videoStoreMuxer) writeH265(au [][]byte, pts int64) {
-	if e == nil {
-		return
-	}
-	e.mu.Lock()
-	defer e.mu.Unlock()
-
-	var filteredAU [][]byte
-
-	isRandomAccess := false
-
-	for _, nalu := range au {
-		//nolint:mnd
-		typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
-		switch typ {
-		case h265.NALUType_VPS_NUT:
-			e.vps = nalu
-			continue
-
-		case h265.NALUType_SPS_NUT:
-			e.sps = nalu
-			e.spsUnChanged = false
-			continue
-
-		case h265.NALUType_PPS_NUT:
-			e.pps = nalu
-			continue
-
-		case h265.NALUType_AUD_NUT:
-			continue
-
-		case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
-			isRandomAccess = true
-		case h265.NALUType_TRAIL_N,
-			h265.NALUType_TRAIL_R,
-			h265.NALUType_TSA_N,
-			h265.NALUType_TSA_R,
-			h265.NALUType_STSA_N,
-			h265.NALUType_STSA_R,
-			h265.NALUType_RADL_N,
-			h265.NALUType_RADL_R,
-			h265.NALUType_RASL_N,
-			h265.NALUType_RASL_R,
-			h265.NALUType_RSV_VCL_N10,
-			h265.NALUType_RSV_VCL_N12,
-			h265.NALUType_RSV_VCL_N14,
-			h265.NALUType_RSV_VCL_R11,
-			h265.NALUType_RSV_VCL_R13,
-			h265.NALUType_RSV_VCL_R15,
-			h265.NALUType_BLA_W_LP,
-			h265.NALUType_BLA_W_RADL,
-			h265.NALUType_BLA_N_LP,
-			h265.NALUType_RSV_IRAP_VCL22,
-			h265.NALUType_RSV_IRAP_VCL23,
-			h265.NALUType_EOS_NUT,
-			h265.NALUType_EOB_NUT,
-			h265.NALUType_FD_NUT,
-			h265.NALUType_PREFIX_SEI_NUT,
-			h265.NALUType_SUFFIX_SEI_NUT,
-			h265.NALUType_AggregationUnit,
-			h265.NALUType_FragmentationUnit,
-			h265.NALUType_PACI:
-		}
-
-		filteredAU = append(filteredAU, nalu)
-	}
-
-	au = filteredAU
-
-	if au == nil {
-		return
-	}
-
-	if err := e.maybeReInitVideoStore(); err != nil {
-		e.logger.Debugf("unable to init video store: %s", err.Error())
-		return
-	}
-
-	// add VPS, SPS and PPS before random access au
-	if isRandomAccess {
-		au = append([][]byte{e.vps, e.sps, e.pps}, au...)
-	}
-
-	if e.dtsExtractor == nil {
-		// skip samples silently until we find one with a IDR
-		if !isRandomAccess {
-			return
-		}
-		e.dtsExtractor = h265.NewDTSExtractor2()
-	}
-
-	dts, err := e.dtsExtractor.Extract(au, pts)
-	if err != nil {
-		e.logger.Errorf("error extracting dts: %s", err)
-		return
-	}
-
-	// h265 uses the same annexb format as h264
-	nalu, err := h264.AnnexBMarshal(au)
-	if err != nil {
-		e.logger.Errorf("failed to marshal annex b: %s", err)
-		return
-	}
-	err = e.vs.WritePacket(nalu, pts, dts, isRandomAccess)
-	if err != nil {
-		e.logger.Errorf("error writing packet to segmenter: %s", err)
-	}
-}
-
-func (e *videoStoreMuxer) writeH264(au [][]byte, pts int64) {
-	if e == nil {
-		return
-	}
-	e.mu.Lock()
-	defer e.mu.Unlock()
-	var filteredAU [][]byte
-	nonIDRPresent := false
-	idrPresent := false
-
-	for _, nalu := range au {
-		//nolint:mnd
-		typ := h264.NALUType(nalu[0] & 0x1F)
-		switch typ {
-		case h264.NALUTypeSPS:
-			e.sps = nalu
-			e.spsUnChanged = false
-			continue
-
-		case h264.NALUTypePPS:
-			e.pps = nalu
-			continue
-
-		case h264.NALUTypeAccessUnitDelimiter:
-			continue
-
-		case h264.NALUTypeIDR:
-			idrPresent = true
-
-		case h264.NALUTypeNonIDR:
-			nonIDRPresent = true
-		case h264.NALUTypeDataPartitionA,
-			h264.NALUTypeDataPartitionB,
-			h264.NALUTypeDataPartitionC,
-			h264.NALUTypeSEI,
-			h264.NALUTypeEndOfSequence,
-			h264.NALUTypeEndOfStream,
-			h264.NALUTypeFillerData,
-			h264.NALUTypeSPSExtension,
-			h264.NALUTypePrefix,
-			h264.NALUTypeSubsetSPS,
-			h264.NALUTypeReserved16,
-			h264.NALUTypeReserved17,
-			h264.NALUTypeReserved18,
-			h264.NALUTypeSliceLayerWithoutPartitioning,
-			h264.NALUTypeSliceExtension,
-			h264.NALUTypeSliceExtensionDepth,
-			h264.NALUTypeReserved22,
-			h264.NALUTypeReserved23,
-			h264.NALUTypeSTAPA,
-			h264.NALUTypeSTAPB,
-			h264.NALUTypeMTAP16,
-			h264.NALUTypeMTAP24,
-			h264.NALUTypeFUA,
-			h264.NALUTypeFUB:
-		}
-
-		filteredAU = append(filteredAU, nalu)
-	}
-
-	au = filteredAU
-
-	if au == nil || (!nonIDRPresent && !idrPresent) {
-		return
-	}
-
-	if err := e.maybeReInitVideoStore(); err != nil {
-		e.logger.Debugf("unable to init video store: %s", err.Error())
-		return
-	}
-
-	// add SPS and PPS before access unit that contains an IDR
-	if idrPresent {
-		au = append([][]byte{e.sps, e.pps}, au...)
-	}
-
-	if e.dtsExtractor == nil {
-		// skip samples silently until we find one with a IDR
-		if !idrPresent {
-			return
-		}
-		e.dtsExtractor = h264.NewDTSExtractor2()
-	}
-
-	dts, err := e.dtsExtractor.Extract(au, pts)
-	if err != nil {
-		return
-	}
-
-	packed, err := h264.AnnexBMarshal(au)
-	if err != nil {
-		e.logger.Errorf("AnnexBMarshal err: %s", err.Error())
-		return
-	}
-	err = e.vs.WritePacket(packed, pts, dts, idrPresent)
-	if err != nil {
-		e.logger.Errorf("error writing packet to segmenter: %s", err)
-	}
-}
-
-func (e *videoStoreMuxer) Save(ctx context.Context, r *videostore.SaveRequest) (*videostore.SaveResponse, error) {
-	e.mu.Lock()
-	defer e.mu.Unlock()
-	if e.vs == nil {
-		return nil, errors.New("video-store uninitialized")
-	}
-	return e.vs.Save(ctx, r)
-}
-
-func (e *videoStoreMuxer) Fetch(ctx context.Context, r *videostore.FetchRequest) (*videostore.FetchResponse, error) {
-	e.mu.Lock()
-	defer e.mu.Unlock()
-	if e.vs == nil {
-		return nil, errors.New("video-store uninitialized")
-	}
-	return e.vs.Fetch(ctx, r)
-}
-
-func (e *videoStoreMuxer) CloseVideoStore() {
-	e.mu.Lock()
-	defer e.mu.Unlock()
-	if e.vs == nil {
-		return
-	}
-	e.vs.Close()
+func (rc *rtspCamera) DoCommand(_ context.Context, _ map[string]interface{}) (map[string]interface{}, error) {
+	return nil, errors.New("not implemented")
 }
diff --git a/videorequest.go b/videorequest.go
new file mode 100644
index 00000000..567fd191
--- /dev/null
+++ b/videorequest.go
@@ -0,0 +1,104 @@
+package viamrtsp
+
+import (
+	"context"
+	"sync"
+
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+)
+
+type videoRequest struct {
+	logger logging.Logger
+
+	mu      sync.Mutex
+	mux     registry.Mux
+	started bool
+	cancel  context.CancelFunc
+}
+
+func (vr *videoRequest) active() bool {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+	return vr.mux != nil
+}
+
+func (vr *videoRequest) newRequest(mux registry.Mux) (context.Context, error) {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+	if vr.mux != nil {
+		return nil, registry.ErrBusy
+	}
+	ctx, cancel := context.WithCancel(context.Background())
+	vr.mux = mux
+	vr.started = false
+	vr.cancel = cancel
+	return ctx, nil
+}
+
+func (vr *videoRequest) cancelRequest(mux registry.Mux) error {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+	if vr.mux == nil {
+		return nil
+	}
+	if vr.mux != mux {
+		return registry.ErrNotFound
+	}
+	vr.cancel()
+	vr.mux = nil
+	vr.started = false
+	vr.cancel = nil
+	return nil
+}
+
+func (vr *videoRequest) write(codec videostore.CodecType, initialParameters [][]byte, au [][]byte, pts int64) {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+	if vr.mux == nil {
+		return
+	}
+
+	if !vr.started {
+		if err := vr.mux.Start(codec, initialParameters); err != nil {
+			vr.logger.Errorf("codec: %s, failed to start Mux: %s", codec, err.Error())
+			return
+		}
+		vr.started = true
+	}
+	if err := vr.mux.WritePacket(codec, au, pts); err != nil {
+		vr.logger.Errorf("codec: %s, videostore WritePacket returned error, err: %s", codec, err.Error())
+	}
+}
+
+func (vr *videoRequest) stop() {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+	if vr.mux != nil {
+		if err := vr.mux.Stop(); err != nil {
+			vr.logger.Errorf("error stopping mux: %s", err.Error())
+		}
+	}
+	vr.started = false
+}
+
+func (vr *videoRequest) clear() {
+	vr.mu.Lock()
+	defer vr.mu.Unlock()
+
+	if vr.mux == nil {
+		return
+	}
+
+	if err := vr.mux.Stop(); err != nil {
+		vr.logger.Errorf("error stopping mux: %s", err.Error())
+	}
+
+	if vr.cancel != nil {
+		vr.cancel()
+	}
+	vr.mux = nil
+	vr.started = false
+	vr.cancel = nil
+}
diff --git a/videostore/config.go b/videostore/config.go
new file mode 100644
index 00000000..62356841
--- /dev/null
+++ b/videostore/config.go
@@ -0,0 +1,71 @@
+package videostore
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/utils"
+)
+
+// Config is the config for videostore.
+type Config struct {
+	Camera  *string `json:"camera,omitempty"`
+	Storage Storage `json:"storage"`
+}
+
+// Storage is the storage subconfig for videostore.
+type Storage struct {
+	SizeGB      int    `json:"size_gb"`
+	UploadPath  string `json:"upload_path,omitempty"`
+	StoragePath string `json:"storage_path,omitempty"`
+}
+
+// Validate validates the config and returns the resource graph dependencies.
+func (cfg *Config) Validate(path string) ([]string, error) {
+	if cfg.Storage == (Storage{}) {
+		return nil, utils.NewConfigValidationFieldRequiredError(path, "storage")
+	}
+	if cfg.Storage.SizeGB == 0 {
+		return nil, utils.NewConfigValidationFieldRequiredError(path, "size_gb")
+	}
+
+	sConfig, err := applyStorageDefaults(cfg.Storage, "someprefix")
+	if err != nil {
+		return nil, err
+	}
+	if err := sConfig.Validate(); err != nil {
+		return nil, err
+	}
+	// This allows for an implicit camera dependency so we do not need to explicitly
+	// add the camera dependency in the config.
+	if cfg.Camera != nil {
+		return []string{*cfg.Camera}, nil
+	}
+	return []string{}, nil
+}
+
+func applyStorageDefaults(c Storage, name string) (videostore.StorageConfig, error) {
+	var zero videostore.StorageConfig
+	if c.UploadPath == "" {
+		home, err := os.UserHomeDir()
+		if err != nil {
+			return zero, err
+		}
+		c.UploadPath = filepath.Join(home, defaultUploadPath, name)
+	}
+	if c.StoragePath == "" {
+		home, err := os.UserHomeDir()
+		if err != nil {
+			return zero, err
+		}
+		c.StoragePath = filepath.Join(home, defaultStoragePath, name)
+	}
+	return videostore.StorageConfig{
+		SegmentSeconds:       defaultSegmentSeconds,
+		SizeGB:               c.SizeGB,
+		OutputFileNamePrefix: name,
+		UploadPath:           c.UploadPath,
+		StoragePath:          c.StoragePath,
+	}, nil
+}
diff --git a/videostore/mux.go b/videostore/mux.go
new file mode 100644
index 00000000..e96108f8
--- /dev/null
+++ b/videostore/mux.go
@@ -0,0 +1,573 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return fmt.Errorf("resource %s unable to be found in viamrtsp registry err: %s", m.camName.String(), err.Error())
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != videostore.CodecTypeUnknown {
+		return fmt.Errorf("init called when codec already set to %s", vsCodec)
+	}
+	switch codec {
+	case videostore.CodecTypeH264:
+		for _, nalu := range au {
+			if len(nalu) == 0 {
+				continue
+			}
+			//nolint:mnd
+			typ := h264.NALUType(nalu[0] & 0x1F)
+			switch typ {
+			case h264.NALUTypeSPS:
+				m.metadata.sps = nalu
+			case h264.NALUTypePPS:
+				m.metadata.pps = nalu
+			case h264.NALUTypeNonIDR,
+				h264.NALUTypeDataPartitionA,
+				h264.NALUTypeDataPartitionB,
+				h264.NALUTypeDataPartitionC,
+				h264.NALUTypeIDR,
+				h264.NALUTypeSEI,
+				h264.NALUTypeAccessUnitDelimiter,
+				h264.NALUTypeEndOfSequence,
+				h264.NALUTypeEndOfStream,
+				h264.NALUTypeFillerData,
+				h264.NALUTypeSPSExtension,
+				h264.NALUTypePrefix,
+				h264.NALUTypeSubsetSPS,
+				h264.NALUTypeReserved16,
+				h264.NALUTypeReserved17,
+				h264.NALUTypeReserved18,
+				h264.NALUTypeSliceLayerWithoutPartitioning,
+				h264.NALUTypeSliceExtension,
+				h264.NALUTypeSliceExtensionDepth,
+				h264.NALUTypeReserved22,
+				h264.NALUTypeReserved23,
+				h264.NALUTypeSTAPA,
+				h264.NALUTypeSTAPB,
+				h264.NALUTypeMTAP16,
+				h264.NALUTypeMTAP24,
+				h264.NALUTypeFUA,
+				h264.NALUTypeFUB:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeH265:
+		for _, nalu := range au {
+			if len(nalu) == 0 {
+				continue
+			}
+			//nolint:mnd
+			typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+			switch typ {
+			case h265.NALUType_VPS_NUT:
+				m.metadata.vps = nalu
+
+			case h265.NALUType_SPS_NUT:
+				m.metadata.sps = nalu
+
+			case h265.NALUType_PPS_NUT:
+				m.metadata.pps = nalu
+			case h265.NALUType_TRAIL_N,
+				h265.NALUType_TRAIL_R,
+				h265.NALUType_TSA_N,
+				h265.NALUType_TSA_R,
+				h265.NALUType_STSA_N,
+				h265.NALUType_STSA_R,
+				h265.NALUType_RADL_N,
+				h265.NALUType_RADL_R,
+				h265.NALUType_RASL_N,
+				h265.NALUType_RASL_R,
+				h265.NALUType_RSV_VCL_N10,
+				h265.NALUType_RSV_VCL_N12,
+				h265.NALUType_RSV_VCL_N14,
+				h265.NALUType_RSV_VCL_R11,
+				h265.NALUType_RSV_VCL_R13,
+				h265.NALUType_RSV_VCL_R15,
+				h265.NALUType_BLA_W_LP,
+				h265.NALUType_BLA_W_RADL,
+				h265.NALUType_BLA_N_LP,
+				h265.NALUType_IDR_W_RADL,
+				h265.NALUType_IDR_N_LP,
+				h265.NALUType_CRA_NUT,
+				h265.NALUType_RSV_IRAP_VCL22,
+				h265.NALUType_RSV_IRAP_VCL23,
+				h265.NALUType_AUD_NUT,
+				h265.NALUType_EOS_NUT,
+				h265.NALUType_EOB_NUT,
+				h265.NALUType_FD_NUT,
+				h265.NALUType_PREFIX_SEI_NUT,
+				h265.NALUType_SUFFIX_SEI_NUT,
+				h265.NALUType_AggregationUnit,
+				h265.NALUType_FragmentationUnit,
+				h265.NALUType_PACI:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+	m.codec.Store(int64(codec))
+	return nil
+}
+
+func (m *rawSegmenterMux) WritePacket(codec videostore.CodecType, au [][]byte, pts int64) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec == videostore.CodecTypeUnknown {
+		return errors.New("WritePacket called before Init")
+	}
+
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != codec {
+		return errors.New("WritePacket called with different codec than Init")
+	}
+
+	switch codec {
+	case videostore.CodecTypeH264:
+		return m.writeH264(au, pts)
+	case videostore.CodecTypeH265:
+		return m.writeH265(au, pts)
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+}
+
+func (m *rawSegmenterMux) Stop() error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if err := m.rawSeg.Close(); err != nil {
+		return err
+	}
+	m.codec.Store(int64(videostore.CodecTypeUnknown))
+	m.metadata = metadata{}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH265(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+
+	isRandomAccess := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+		switch typ {
+		case h265.NALUType_VPS_NUT:
+			m.metadata.vps = nalu
+			continue
+
+		case h265.NALUType_SPS_NUT:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h265.NALUType_PPS_NUT:
+			m.metadata.pps = nalu
+			continue
+
+		case h265.NALUType_AUD_NUT:
+			continue
+
+		case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
+			isRandomAccess = true
+		case h265.NALUType_TRAIL_N,
+			h265.NALUType_TRAIL_R,
+			h265.NALUType_TSA_N,
+			h265.NALUType_TSA_R,
+			h265.NALUType_STSA_N,
+			h265.NALUType_STSA_R,
+			h265.NALUType_RADL_N,
+			h265.NALUType_RADL_R,
+			h265.NALUType_RASL_N,
+			h265.NALUType_RASL_R,
+			h265.NALUType_RSV_VCL_N10,
+			h265.NALUType_RSV_VCL_N12,
+			h265.NALUType_RSV_VCL_N14,
+			h265.NALUType_RSV_VCL_R11,
+			h265.NALUType_RSV_VCL_R13,
+			h265.NALUType_RSV_VCL_R15,
+			h265.NALUType_BLA_W_LP,
+			h265.NALUType_BLA_W_RADL,
+			h265.NALUType_BLA_N_LP,
+			h265.NALUType_RSV_IRAP_VCL22,
+			h265.NALUType_RSV_IRAP_VCL23,
+			h265.NALUType_EOS_NUT,
+			h265.NALUType_EOB_NUT,
+			h265.NALUType_FD_NUT,
+			h265.NALUType_PREFIX_SEI_NUT,
+			h265.NALUType_SUFFIX_SEI_NUT,
+			h265.NALUType_AggregationUnit,
+			h265.NALUType_FragmentationUnit,
+			h265.NALUType_PACI:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		return err
+	}
+
+	// add VPS, SPS and PPS before random access au
+	if isRandomAccess {
+		au = append([][]byte{m.metadata.vps, m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !isRandomAccess {
+			return nil
+		}
+		m.metadata.dtsExtractor = h265.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Errorf("error extracting dts: %s", err)
+		return nil
+	}
+
+	// h265 uses the same annexb format as h264
+	nalu, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("failed to marshal annex b: %s", err)
+		return err
+	}
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	// we need to subtract the first pts & dts from the current one so that the
+	// first pts & dts are zero. Otherwise the ffmpeg segmenter will create an mp4 file
+	// that is a black screen for a few seconds (we believe the amount of time represented by the non zero
+	// timestamps)
+	err = m.rawSeg.WritePacket(nalu, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, isRandomAccess)
+	if err != nil {
+		m.logger.Errorf("error writing packet to segmenter: %s", err)
+	}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH264(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+	nonIDRPresent := false
+	idrPresent := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h264.NALUType(nalu[0] & 0x1F)
+		switch typ {
+		case h264.NALUTypeSPS:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h264.NALUTypePPS:
+			m.metadata.pps = nalu
+			continue
+
+		case h264.NALUTypeAccessUnitDelimiter:
+			continue
+
+		case h264.NALUTypeIDR:
+			idrPresent = true
+
+		case h264.NALUTypeNonIDR:
+			nonIDRPresent = true
+		case h264.NALUTypeDataPartitionA,
+			h264.NALUTypeDataPartitionB,
+			h264.NALUTypeDataPartitionC,
+			h264.NALUTypeSEI,
+			h264.NALUTypeEndOfSequence,
+			h264.NALUTypeEndOfStream,
+			h264.NALUTypeFillerData,
+			h264.NALUTypeSPSExtension,
+			h264.NALUTypePrefix,
+			h264.NALUTypeSubsetSPS,
+			h264.NALUTypeReserved16,
+			h264.NALUTypeReserved17,
+			h264.NALUTypeReserved18,
+			h264.NALUTypeSliceLayerWithoutPartitioning,
+			h264.NALUTypeSliceExtension,
+			h264.NALUTypeSliceExtensionDepth,
+			h264.NALUTypeReserved22,
+			h264.NALUTypeReserved23,
+			h264.NALUTypeSTAPA,
+			h264.NALUTypeSTAPB,
+			h264.NALUTypeMTAP16,
+			h264.NALUTypeMTAP24,
+			h264.NALUTypeFUA,
+			h264.NALUTypeFUB:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil || (!nonIDRPresent && !idrPresent) {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		m.logger.Debugf("unable to init video store: %s", err.Error())
+		return nil
+	}
+
+	// add SPS and PPS before access unit that contains an IDR
+	if idrPresent {
+		au = append([][]byte{m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !idrPresent {
+			return nil
+		}
+		m.metadata.dtsExtractor = h264.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Debugf("dtsExtractor Extract err: %s", err.Error())
+		return nil
+	}
+
+	packed, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("AnnexBMarshal err: %s", err.Error())
+		return err
+	}
+
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	// we need to subtract the first pts & dts from the current one so that the
+	// first pts & dts are zero. Otherwise the ffmpeg segmenter will create an mp4 file
+	// that is a black screen for a few seconds (we believe the amount of time represented by the non zero
+	// timestamps)
+	err = m.rawSeg.WritePacket(packed, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, idrPresent)
+	if err != nil {
+		m.logger.Errorf("error writing packet to segmenter: %s", err)
+	}
+	return nil
+}
+
+// // maybeReInitVideoStore assumes mu is held by caller.
+func (m *rawSegmenterMux) maybeReInitVideoStore() error {
+	if m.metadata.spsUnChanged {
+		return nil
+	}
+	var width, height int
+	codec := videostore.CodecType(m.codec.Load())
+	switch codec {
+	case videostore.CodecTypeH265:
+		var hsps h265.SPS
+		if err := hsps.Unmarshal(m.metadata.sps); err != nil {
+			m.logger.Debugf("unable to init video store: %s", err.Error())
+			return nil
+		}
+		width, height = hsps.Width(), hsps.Height()
+	case videostore.CodecTypeH264:
+		var hsps h264.SPS
+		if err := hsps.Unmarshal(m.metadata.sps); err != nil {
+			m.logger.Debugf("unable to init video store: %s", err.Error())
+			return nil
+		}
+		width, height = hsps.Width(), hsps.Height()
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid videostore.CodecType")
+	}
+
+	if width <= 0 || height <= 0 {
+		err := errors.New("width and height must both be greater than 0")
+		m.logger.Infof("unable to init video store: %s", err.Error())
+		return nil
+	}
+	// if vs is initialized and the height & width have not changed,
+	// record the sps as unchanged and return
+	if m.metadata.width == width && m.metadata.height == height {
+		m.metadata.spsUnChanged = true
+		return nil
+	}
+
+	// if initialized and the height & width have changed,
+	// close and nil out the videostore
+	if err := m.rawSeg.Close(); err != nil {
+		return err
+	}
+
+	if err := m.rawSeg.Init(codec, width, height); err != nil {
+		return err
+	}
+
+	m.metadata.width = width
+	m.metadata.height = height
+	m.metadata.spsUnChanged = true
+	return nil
+}
diff --git a/videostore/videostore.go b/videostore/videostore.go
new file mode 100644
index 00000000..22be6154
--- /dev/null
+++ b/videostore/videostore.go
@@ -0,0 +1,216 @@
+// Package videostore stores video and allows it to be queried
+package videostore
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"time"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/components/camera"
+	"go.viam.com/rdk/components/generic"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+)
+
+const (
+	defaultSegmentSeconds      = 30 // seconds
+	defaultUploadPath          = ".viam/capture/video-upload"
+	defaultStoragePath         = ".viam/video-storage-viamrtsp"
+	maxGRPCSize                = 1024 * 1024 * 32 // bytes
+	videoStoreInitCloseTimeout = time.Second * 10
+)
+
+// Model is videostore's Viam model.
+var Model = resource.ModelNamespace("viam").WithFamily("viamrtsp").WithModel("video-store")
+
+func init() {
+	resource.RegisterComponent(generic.API, Model, resource.Registration[resource.Resource, *Config]{
+		Constructor: New,
+	})
+}
+
+type service struct {
+	resource.AlwaysRebuild
+	name   resource.Name
+	logger logging.Logger
+	vs     videostore.VideoStore
+	rsMux  *rawSegmenterMux
+}
+
+// New creates a new videostore.
+func New(_ context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (resource.Resource, error) {
+	newConf, err := resource.NativeConfig[*Config](conf)
+	if err != nil {
+		logger.Error(err.Error())
+		return nil, err
+	}
+	sc, err := applyStorageDefaults(newConf.Storage, conf.ResourceName().Name)
+	if err != nil {
+		return nil, err
+	}
+	if err := sc.Validate(); err != nil {
+		return nil, err
+	}
+
+	var vs videostore.VideoStore
+	var mux *rawSegmenterMux
+	if newConf.Camera != nil {
+		c, err := camera.FromDependencies(deps, *newConf.Camera)
+		if err != nil {
+			return nil, err
+		}
+
+		rtpVs, err := videostore.NewRTPVideoStore(videostore.Config{
+			Type:    videostore.SourceTypeRTP,
+			Storage: sc,
+		}, logger)
+		if err != nil {
+			return nil, err
+		}
+
+		mux = newRawSegmenterMux(rtpVs.Segmenter(), c.Name(), logger)
+		if err := mux.init(); err != nil {
+			rtpVs.Close()
+			return nil, err
+		}
+		vs = rtpVs
+	} else {
+		vs, err = videostore.NewReadOnlyVideoStore(videostore.Config{
+			Type:    videostore.SourceTypeReadOnly,
+			Storage: sc,
+		}, logger)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	s := &service{
+		name:   conf.ResourceName(),
+		logger: logger,
+		vs:     vs,
+		rsMux:  mux,
+	}
+	return s, nil
+}
+
+func (s *service) Name() resource.Name {
+	return s.name
+}
+
+func (s *service) Close(_ context.Context) error {
+	if err := s.rsMux.close(); err != nil {
+		return err
+	}
+	s.vs.Close()
+	return nil
+}
+
+func (s *service) DoCommand(ctx context.Context, command map[string]interface{}) (map[string]interface{}, error) {
+	cmd, ok := command["command"].(string)
+	if !ok {
+		return nil, errors.New("invalid command type")
+	}
+
+	switch cmd {
+	// Save command is used to concatenate video clips between the given timestamps.
+	// The concatenated video file is then uploaded to the cloud the upload path.
+	// The response contains the name of the uploaded file.
+	case "save":
+		s.logger.Debug("save command received")
+		req, err := toSaveCommand(command)
+		if err != nil {
+			return nil, err
+		}
+
+		res, err := s.vs.Save(ctx, req)
+		if err != nil {
+			return nil, err
+		}
+
+		ret := map[string]interface{}{
+			"command":  "save",
+			"filename": res.Filename,
+		}
+
+		if req.Async {
+			ret["status"] = "async"
+		}
+		return ret, nil
+	case "fetch":
+		s.logger.Debug("fetch command received")
+		req, err := toFetchCommand(command)
+		if err != nil {
+			return nil, err
+		}
+		res, err := s.vs.Fetch(ctx, req)
+		if err != nil {
+			return nil, err
+		}
+		if len(res.Video) > maxGRPCSize {
+			return nil, errors.New("video file size exceeds max grpc size")
+		}
+		// TODO(seanp): Do we need to encode the video bytes to base64?
+		videoBytesBase64 := base64.StdEncoding.EncodeToString(res.Video)
+		return map[string]interface{}{
+			"command": "fetch",
+			"video":   videoBytesBase64,
+		}, nil
+	default:
+		return nil, errors.New("invalid command")
+	}
+}
+
+func toSaveCommand(command map[string]interface{}) (*videostore.SaveRequest, error) {
+	fromStr, ok := command["from"].(string)
+	if !ok {
+		return nil, errors.New("from timestamp not found")
+	}
+	from, err := videostore.ParseDateTimeString(fromStr)
+	if err != nil {
+		return nil, err
+	}
+	toStr, ok := command["to"].(string)
+	if !ok {
+		return nil, errors.New("to timestamp not found")
+	}
+	to, err := videostore.ParseDateTimeString(toStr)
+	if err != nil {
+		return nil, err
+	}
+	metadata, ok := command["metadata"].(string)
+	if !ok {
+		metadata = ""
+	}
+	async, ok := command["async"].(bool)
+	if !ok {
+		async = false
+	}
+	return &videostore.SaveRequest{
+		From:     from,
+		To:       to,
+		Metadata: metadata,
+		Async:    async,
+	}, nil
+}
+
+func toFetchCommand(command map[string]interface{}) (*videostore.FetchRequest, error) {
+	fromStr, ok := command["from"].(string)
+	if !ok {
+		return nil, errors.New("from timestamp not found")
+	}
+	from, err := videostore.ParseDateTimeString(fromStr)
+	if err != nil {
+		return nil, err
+	}
+	toStr, ok := command["to"].(string)
+	if !ok {
+		return nil, errors.New("to timestamp not found")
+	}
+	to, err := videostore.ParseDateTimeString(toStr)
+	if err != nil {
+		return nil, err
+	}
+	return &videostore.FetchRequest{From: from, To: to}, nil
+}


Reviews:
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T21:23:57Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T21:34:12Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:27:12Z
user: login: seanavery
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T18:55:14Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:09:34Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:12:32Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:14:24Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:22:39Z
user: login: seanavery
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T20:31:05Z
user: login: randhid
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:36:19Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:38:25Z
user: login: randhid
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T21:13:10Z
user: login: nicksanford
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T22:05:27Z

Review comments:
diff_hunk: @@ -46,7 +46,7 @@ func mainWithArgs(ctx context.Context, _ []string, logger logging.Logger) error
 		},
 	}
 
-	myRobot, err := robotimpl.New(ctx, conf, logger)
path: cmd/remote/cmd.go
user: login: nicksanford
body: signature of this function changed in https://github.com/viamrobotics/rdk/pull/4782/files
created_at: 2025-03-10T21:23:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+// Package registry provides a means for videostores to find viamrtsp cameras within the same OS process
+package registry
+
+import (
+	"context"
+	"errors"
+	"sync"
+
+	"github.com/viam-modules/video-store/videostore"
+)
+
+// Mux is how viamrtsp writes it's video data to a video-store that has reqeusted it.
+type Mux interface {
+	// Start starts saving an rtsp stream's video
+	Start(codec videostore.CodecType, initialParameters [][]byte) error
path: registry/registry.go
user: login: nicksanford
body: This is going to be [videostore.*RawSegmenter](https://github.com/viam-modules/video-store/blob/main/videostore/rawsegmenter.go#L19)
created_at: 2025-03-10T21:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package viamrtsp
path: videorequest.go
user: login: nicksanford
body: this managest the lifecycle of the videostore's request for video
created_at: 2025-03-11T15:27:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
path: videostore/mux.go
user: login: seanavery
body: Trying to understand the flow better here. Is there any possibility of race condition in`registry.Global` reference between vaimrtrtsp and video store spin up? Is it guaranteed that the camera will be registered before the muxer attempts to reference it here?
created_at: 2025-03-11T18:24:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return err
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != videostore.CodecTypeUnknown {
+		return fmt.Errorf("init called when codec already set to %s", vsCodec)
+	}
+	switch codec {
+	case videostore.CodecTypeH264:
+		for _, nalu := range au {
+			//nolint:mnd
+			typ := h264.NALUType(nalu[0] & 0x1F)
+			switch typ {
+			case h264.NALUTypeSPS:
+				m.metadata.sps = nalu
+			case h264.NALUTypePPS:
+				m.metadata.pps = nalu
+			case h264.NALUTypeNonIDR,
+				h264.NALUTypeDataPartitionA,
+				h264.NALUTypeDataPartitionB,
+				h264.NALUTypeDataPartitionC,
+				h264.NALUTypeIDR,
+				h264.NALUTypeSEI,
+				h264.NALUTypeAccessUnitDelimiter,
+				h264.NALUTypeEndOfSequence,
+				h264.NALUTypeEndOfStream,
+				h264.NALUTypeFillerData,
+				h264.NALUTypeSPSExtension,
+				h264.NALUTypePrefix,
+				h264.NALUTypeSubsetSPS,
+				h264.NALUTypeReserved16,
+				h264.NALUTypeReserved17,
+				h264.NALUTypeReserved18,
+				h264.NALUTypeSliceLayerWithoutPartitioning,
+				h264.NALUTypeSliceExtension,
+				h264.NALUTypeSliceExtensionDepth,
+				h264.NALUTypeReserved22,
+				h264.NALUTypeReserved23,
+				h264.NALUTypeSTAPA,
+				h264.NALUTypeSTAPB,
+				h264.NALUTypeMTAP16,
+				h264.NALUTypeMTAP24,
+				h264.NALUTypeFUA,
+				h264.NALUTypeFUB:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeH265:
+		for _, nalu := range au {
+			//nolint:mnd
+			typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+			switch typ {
+			case h265.NALUType_VPS_NUT:
+				m.metadata.vps = nalu
+
+			case h265.NALUType_SPS_NUT:
+				m.metadata.sps = nalu
+
+			case h265.NALUType_PPS_NUT:
+				m.metadata.pps = nalu
+			case h265.NALUType_TRAIL_N,
+				h265.NALUType_TRAIL_R,
+				h265.NALUType_TSA_N,
+				h265.NALUType_TSA_R,
+				h265.NALUType_STSA_N,
+				h265.NALUType_STSA_R,
+				h265.NALUType_RADL_N,
+				h265.NALUType_RADL_R,
+				h265.NALUType_RASL_N,
+				h265.NALUType_RASL_R,
+				h265.NALUType_RSV_VCL_N10,
+				h265.NALUType_RSV_VCL_N12,
+				h265.NALUType_RSV_VCL_N14,
+				h265.NALUType_RSV_VCL_R11,
+				h265.NALUType_RSV_VCL_R13,
+				h265.NALUType_RSV_VCL_R15,
+				h265.NALUType_BLA_W_LP,
+				h265.NALUType_BLA_W_RADL,
+				h265.NALUType_BLA_N_LP,
+				h265.NALUType_IDR_W_RADL,
+				h265.NALUType_IDR_N_LP,
+				h265.NALUType_CRA_NUT,
+				h265.NALUType_RSV_IRAP_VCL22,
+				h265.NALUType_RSV_IRAP_VCL23,
+				h265.NALUType_AUD_NUT,
+				h265.NALUType_EOS_NUT,
+				h265.NALUType_EOB_NUT,
+				h265.NALUType_FD_NUT,
+				h265.NALUType_PREFIX_SEI_NUT,
+				h265.NALUType_SUFFIX_SEI_NUT,
+				h265.NALUType_AggregationUnit,
+				h265.NALUType_FragmentationUnit,
+				h265.NALUType_PACI:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+	m.codec.Store(int64(codec))
+	return nil
+}
+
+func (m *rawSegmenterMux) WritePacket(codec videostore.CodecType, au [][]byte, pts int64) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec == videostore.CodecTypeUnknown {
+		return errors.New("WritePacket called before Init")
+	}
+
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != codec {
+		return errors.New("WritePacket called with different codec than Init")
+	}
+
+	switch codec {
+	case videostore.CodecTypeH264:
+		return m.writeH264(au, pts)
+	case videostore.CodecTypeH265:
+		return m.writeH265(au, pts)
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+}
+
+func (m *rawSegmenterMux) Stop() error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if err := m.rawSeg.Close(); err != nil {
+		return err
+	}
+	m.codec.Store(int64(videostore.CodecTypeUnknown))
+	m.metadata = metadata{}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH265(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+
+	isRandomAccess := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+		switch typ {
+		case h265.NALUType_VPS_NUT:
+			m.metadata.vps = nalu
+			continue
+
+		case h265.NALUType_SPS_NUT:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h265.NALUType_PPS_NUT:
+			m.metadata.pps = nalu
+			continue
+
+		case h265.NALUType_AUD_NUT:
+			continue
+
+		case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
+			isRandomAccess = true
+		case h265.NALUType_TRAIL_N,
+			h265.NALUType_TRAIL_R,
+			h265.NALUType_TSA_N,
+			h265.NALUType_TSA_R,
+			h265.NALUType_STSA_N,
+			h265.NALUType_STSA_R,
+			h265.NALUType_RADL_N,
+			h265.NALUType_RADL_R,
+			h265.NALUType_RASL_N,
+			h265.NALUType_RASL_R,
+			h265.NALUType_RSV_VCL_N10,
+			h265.NALUType_RSV_VCL_N12,
+			h265.NALUType_RSV_VCL_N14,
+			h265.NALUType_RSV_VCL_R11,
+			h265.NALUType_RSV_VCL_R13,
+			h265.NALUType_RSV_VCL_R15,
+			h265.NALUType_BLA_W_LP,
+			h265.NALUType_BLA_W_RADL,
+			h265.NALUType_BLA_N_LP,
+			h265.NALUType_RSV_IRAP_VCL22,
+			h265.NALUType_RSV_IRAP_VCL23,
+			h265.NALUType_EOS_NUT,
+			h265.NALUType_EOB_NUT,
+			h265.NALUType_FD_NUT,
+			h265.NALUType_PREFIX_SEI_NUT,
+			h265.NALUType_SUFFIX_SEI_NUT,
+			h265.NALUType_AggregationUnit,
+			h265.NALUType_FragmentationUnit,
+			h265.NALUType_PACI:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		return err
+	}
+
+	// add VPS, SPS and PPS before random access au
+	if isRandomAccess {
+		au = append([][]byte{m.metadata.vps, m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !isRandomAccess {
+			return nil
+		}
+		m.metadata.dtsExtractor = h265.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Errorf("error extracting dts: %s", err)
+		return nil
+	}
+
+	// h265 uses the same annexb format as h264
+	nalu, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("failed to marshal annex b: %s", err)
+		return err
+	}
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	err = m.rawSeg.WritePacket(nalu, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, isRandomAccess)
+	if err != nil {
+		m.logger.Errorf("error writing packet to segmenter: %s", err)
+	}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH264(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+	nonIDRPresent := false
+	idrPresent := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h264.NALUType(nalu[0] & 0x1F)
+		switch typ {
+		case h264.NALUTypeSPS:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h264.NALUTypePPS:
+			m.metadata.pps = nalu
+			continue
+
+		case h264.NALUTypeAccessUnitDelimiter:
+			continue
+
+		case h264.NALUTypeIDR:
+			idrPresent = true
+
+		case h264.NALUTypeNonIDR:
+			nonIDRPresent = true
+		case h264.NALUTypeDataPartitionA,
+			h264.NALUTypeDataPartitionB,
+			h264.NALUTypeDataPartitionC,
+			h264.NALUTypeSEI,
+			h264.NALUTypeEndOfSequence,
+			h264.NALUTypeEndOfStream,
+			h264.NALUTypeFillerData,
+			h264.NALUTypeSPSExtension,
+			h264.NALUTypePrefix,
+			h264.NALUTypeSubsetSPS,
+			h264.NALUTypeReserved16,
+			h264.NALUTypeReserved17,
+			h264.NALUTypeReserved18,
+			h264.NALUTypeSliceLayerWithoutPartitioning,
+			h264.NALUTypeSliceExtension,
+			h264.NALUTypeSliceExtensionDepth,
+			h264.NALUTypeReserved22,
+			h264.NALUTypeReserved23,
+			h264.NALUTypeSTAPA,
+			h264.NALUTypeSTAPB,
+			h264.NALUTypeMTAP16,
+			h264.NALUTypeMTAP24,
+			h264.NALUTypeFUA,
+			h264.NALUTypeFUB:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil || (!nonIDRPresent && !idrPresent) {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		m.logger.Debugf("unable to init video store: %s", err.Error())
+		return nil
+	}
+
+	// add SPS and PPS before access unit that contains an IDR
+	if idrPresent {
+		au = append([][]byte{m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !idrPresent {
+			return nil
+		}
+		m.metadata.dtsExtractor = h264.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Debugf("dtsExtractor Extract err: %s", err.Error())
+		return nil
+	}
+
+	packed, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("AnnexBMarshal err: %s", err.Error())
+		return err
+	}
+
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	err = m.rawSeg.WritePacket(packed, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, idrPresent)
path: videostore/mux.go
user: login: seanavery
body: Mind adding a comment about why we need to adjust timestamps?
created_at: 2025-03-11T18:46:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return err
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
path: videostore/mux.go
user: login: seanavery
body: Do wee need a separate `Start` method here even though we handle metadata updates in `Write`?
created_at: 2025-03-11T18:49:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,216 @@
+// Package videostore stores video and allows it to be queried
+package videostore
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"time"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/components/camera"
+	"go.viam.com/rdk/components/generic"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+)
+
+const (
+	defaultSegmentSeconds      = 30 // seconds
+	defaultUploadPath          = ".viam/capture/video-upload"
+	defaultStoragePath         = ".viam/video-storage-viamrtsp"
path: videostore/videostore.go
user: login: seanavery
body: Should we delete the `defaultStoragePath` and `maxGRPCSize` consts in rtsp.go file now?
created_at: 2025-03-11T18:54:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,216 @@
+// Package videostore stores video and allows it to be queried
+package videostore
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"time"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/components/camera"
+	"go.viam.com/rdk/components/generic"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+)
+
+const (
+	defaultSegmentSeconds      = 30 // seconds
+	defaultUploadPath          = ".viam/capture/video-upload"
+	defaultStoragePath         = ".viam/video-storage-viamrtsp"
path: videostore/videostore.go
user: login: nicksanford
body: good call, done
created_at: 2025-03-11T19:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return err
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != videostore.CodecTypeUnknown {
+		return fmt.Errorf("init called when codec already set to %s", vsCodec)
+	}
+	switch codec {
+	case videostore.CodecTypeH264:
+		for _, nalu := range au {
+			//nolint:mnd
+			typ := h264.NALUType(nalu[0] & 0x1F)
+			switch typ {
+			case h264.NALUTypeSPS:
+				m.metadata.sps = nalu
+			case h264.NALUTypePPS:
+				m.metadata.pps = nalu
+			case h264.NALUTypeNonIDR,
+				h264.NALUTypeDataPartitionA,
+				h264.NALUTypeDataPartitionB,
+				h264.NALUTypeDataPartitionC,
+				h264.NALUTypeIDR,
+				h264.NALUTypeSEI,
+				h264.NALUTypeAccessUnitDelimiter,
+				h264.NALUTypeEndOfSequence,
+				h264.NALUTypeEndOfStream,
+				h264.NALUTypeFillerData,
+				h264.NALUTypeSPSExtension,
+				h264.NALUTypePrefix,
+				h264.NALUTypeSubsetSPS,
+				h264.NALUTypeReserved16,
+				h264.NALUTypeReserved17,
+				h264.NALUTypeReserved18,
+				h264.NALUTypeSliceLayerWithoutPartitioning,
+				h264.NALUTypeSliceExtension,
+				h264.NALUTypeSliceExtensionDepth,
+				h264.NALUTypeReserved22,
+				h264.NALUTypeReserved23,
+				h264.NALUTypeSTAPA,
+				h264.NALUTypeSTAPB,
+				h264.NALUTypeMTAP16,
+				h264.NALUTypeMTAP24,
+				h264.NALUTypeFUA,
+				h264.NALUTypeFUB:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeH265:
+		for _, nalu := range au {
+			//nolint:mnd
+			typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+			switch typ {
+			case h265.NALUType_VPS_NUT:
+				m.metadata.vps = nalu
+
+			case h265.NALUType_SPS_NUT:
+				m.metadata.sps = nalu
+
+			case h265.NALUType_PPS_NUT:
+				m.metadata.pps = nalu
+			case h265.NALUType_TRAIL_N,
+				h265.NALUType_TRAIL_R,
+				h265.NALUType_TSA_N,
+				h265.NALUType_TSA_R,
+				h265.NALUType_STSA_N,
+				h265.NALUType_STSA_R,
+				h265.NALUType_RADL_N,
+				h265.NALUType_RADL_R,
+				h265.NALUType_RASL_N,
+				h265.NALUType_RASL_R,
+				h265.NALUType_RSV_VCL_N10,
+				h265.NALUType_RSV_VCL_N12,
+				h265.NALUType_RSV_VCL_N14,
+				h265.NALUType_RSV_VCL_R11,
+				h265.NALUType_RSV_VCL_R13,
+				h265.NALUType_RSV_VCL_R15,
+				h265.NALUType_BLA_W_LP,
+				h265.NALUType_BLA_W_RADL,
+				h265.NALUType_BLA_N_LP,
+				h265.NALUType_IDR_W_RADL,
+				h265.NALUType_IDR_N_LP,
+				h265.NALUType_CRA_NUT,
+				h265.NALUType_RSV_IRAP_VCL22,
+				h265.NALUType_RSV_IRAP_VCL23,
+				h265.NALUType_AUD_NUT,
+				h265.NALUType_EOS_NUT,
+				h265.NALUType_EOB_NUT,
+				h265.NALUType_FD_NUT,
+				h265.NALUType_PREFIX_SEI_NUT,
+				h265.NALUType_SUFFIX_SEI_NUT,
+				h265.NALUType_AggregationUnit,
+				h265.NALUType_FragmentationUnit,
+				h265.NALUType_PACI:
+				fallthrough
+			default:
+				return errors.New("invalid nalu")
+			}
+		}
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+	m.codec.Store(int64(codec))
+	return nil
+}
+
+func (m *rawSegmenterMux) WritePacket(codec videostore.CodecType, au [][]byte, pts int64) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec == videostore.CodecTypeUnknown {
+		return errors.New("WritePacket called before Init")
+	}
+
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != codec {
+		return errors.New("WritePacket called with different codec than Init")
+	}
+
+	switch codec {
+	case videostore.CodecTypeH264:
+		return m.writeH264(au, pts)
+	case videostore.CodecTypeH265:
+		return m.writeH265(au, pts)
+	case videostore.CodecTypeUnknown:
+		fallthrough
+	default:
+		return errors.New("invalid codec")
+	}
+}
+
+func (m *rawSegmenterMux) Stop() error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if err := m.rawSeg.Close(); err != nil {
+		return err
+	}
+	m.codec.Store(int64(videostore.CodecTypeUnknown))
+	m.metadata = metadata{}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH265(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+
+	isRandomAccess := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
+		switch typ {
+		case h265.NALUType_VPS_NUT:
+			m.metadata.vps = nalu
+			continue
+
+		case h265.NALUType_SPS_NUT:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h265.NALUType_PPS_NUT:
+			m.metadata.pps = nalu
+			continue
+
+		case h265.NALUType_AUD_NUT:
+			continue
+
+		case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
+			isRandomAccess = true
+		case h265.NALUType_TRAIL_N,
+			h265.NALUType_TRAIL_R,
+			h265.NALUType_TSA_N,
+			h265.NALUType_TSA_R,
+			h265.NALUType_STSA_N,
+			h265.NALUType_STSA_R,
+			h265.NALUType_RADL_N,
+			h265.NALUType_RADL_R,
+			h265.NALUType_RASL_N,
+			h265.NALUType_RASL_R,
+			h265.NALUType_RSV_VCL_N10,
+			h265.NALUType_RSV_VCL_N12,
+			h265.NALUType_RSV_VCL_N14,
+			h265.NALUType_RSV_VCL_R11,
+			h265.NALUType_RSV_VCL_R13,
+			h265.NALUType_RSV_VCL_R15,
+			h265.NALUType_BLA_W_LP,
+			h265.NALUType_BLA_W_RADL,
+			h265.NALUType_BLA_N_LP,
+			h265.NALUType_RSV_IRAP_VCL22,
+			h265.NALUType_RSV_IRAP_VCL23,
+			h265.NALUType_EOS_NUT,
+			h265.NALUType_EOB_NUT,
+			h265.NALUType_FD_NUT,
+			h265.NALUType_PREFIX_SEI_NUT,
+			h265.NALUType_SUFFIX_SEI_NUT,
+			h265.NALUType_AggregationUnit,
+			h265.NALUType_FragmentationUnit,
+			h265.NALUType_PACI:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		return err
+	}
+
+	// add VPS, SPS and PPS before random access au
+	if isRandomAccess {
+		au = append([][]byte{m.metadata.vps, m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !isRandomAccess {
+			return nil
+		}
+		m.metadata.dtsExtractor = h265.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Errorf("error extracting dts: %s", err)
+		return nil
+	}
+
+	// h265 uses the same annexb format as h264
+	nalu, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("failed to marshal annex b: %s", err)
+		return err
+	}
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	err = m.rawSeg.WritePacket(nalu, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, isRandomAccess)
+	if err != nil {
+		m.logger.Errorf("error writing packet to segmenter: %s", err)
+	}
+	return nil
+}
+
+func (m *rawSegmenterMux) writeH264(au [][]byte, pts int64) error {
+	var filteredAU [][]byte
+	nonIDRPresent := false
+	idrPresent := false
+
+	for _, nalu := range au {
+		//nolint:mnd
+		typ := h264.NALUType(nalu[0] & 0x1F)
+		switch typ {
+		case h264.NALUTypeSPS:
+			m.metadata.sps = nalu
+			m.metadata.spsUnChanged = false
+			continue
+
+		case h264.NALUTypePPS:
+			m.metadata.pps = nalu
+			continue
+
+		case h264.NALUTypeAccessUnitDelimiter:
+			continue
+
+		case h264.NALUTypeIDR:
+			idrPresent = true
+
+		case h264.NALUTypeNonIDR:
+			nonIDRPresent = true
+		case h264.NALUTypeDataPartitionA,
+			h264.NALUTypeDataPartitionB,
+			h264.NALUTypeDataPartitionC,
+			h264.NALUTypeSEI,
+			h264.NALUTypeEndOfSequence,
+			h264.NALUTypeEndOfStream,
+			h264.NALUTypeFillerData,
+			h264.NALUTypeSPSExtension,
+			h264.NALUTypePrefix,
+			h264.NALUTypeSubsetSPS,
+			h264.NALUTypeReserved16,
+			h264.NALUTypeReserved17,
+			h264.NALUTypeReserved18,
+			h264.NALUTypeSliceLayerWithoutPartitioning,
+			h264.NALUTypeSliceExtension,
+			h264.NALUTypeSliceExtensionDepth,
+			h264.NALUTypeReserved22,
+			h264.NALUTypeReserved23,
+			h264.NALUTypeSTAPA,
+			h264.NALUTypeSTAPB,
+			h264.NALUTypeMTAP16,
+			h264.NALUTypeMTAP24,
+			h264.NALUTypeFUA,
+			h264.NALUTypeFUB:
+		default:
+			return errors.New("invalid nalu")
+		}
+
+		filteredAU = append(filteredAU, nalu)
+	}
+
+	au = filteredAU
+
+	if au == nil || (!nonIDRPresent && !idrPresent) {
+		return nil
+	}
+
+	if err := m.maybeReInitVideoStore(); err != nil {
+		m.logger.Debugf("unable to init video store: %s", err.Error())
+		return nil
+	}
+
+	// add SPS and PPS before access unit that contains an IDR
+	if idrPresent {
+		au = append([][]byte{m.metadata.sps, m.metadata.pps}, au...)
+	}
+
+	if m.metadata.dtsExtractor == nil {
+		// skip samples silently until we find one with a IDR
+		if !idrPresent {
+			return nil
+		}
+		m.metadata.dtsExtractor = h264.NewDTSExtractor2()
+	}
+
+	dts, err := m.metadata.dtsExtractor.Extract(au, pts)
+	if err != nil {
+		m.logger.Debugf("dtsExtractor Extract err: %s", err.Error())
+		return nil
+	}
+
+	packed, err := h264.AnnexBMarshal(au)
+	if err != nil {
+		m.logger.Errorf("AnnexBMarshal err: %s", err.Error())
+		return err
+	}
+
+	if !m.metadata.firstTimeStampsSet {
+		m.metadata.firstPTS = pts
+		m.metadata.firstDTS = dts
+		m.metadata.firstTimeStampsSet = true
+	}
+	err = m.rawSeg.WritePacket(packed, pts-m.metadata.firstPTS, dts-m.metadata.firstDTS, idrPresent)
path: videostore/mux.go
user: login: nicksanford
body: yep done
created_at: 2025-03-11T19:12:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
path: videostore/mux.go
user: login: nicksanford
body: I don't think so.

If the video-store's config has a camera then the video-store's config will return that camera as a dependency of video-store. Resource graph will not boot video-store until the camera is booted.
created_at: 2025-03-11T19:14:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,559 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return err
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
path: videostore/mux.go
user: login: nicksanford
body: If there are cameras which send sps, pps, vps in the SDP but not in the rtsp stream then we would fail to save the video without this Start call.

This start call ensures that we remember the sps, vps and pps from the SDP in case we never see new initial parameters later in the RTSP stream.

I haven't seen a camera with that behavior but it seemed worth the additional complexity to prevent a regression in some camera which is already working in the wild we don't have access to in the office.
created_at: 2025-03-11T19:22:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+package videostore
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/utils"
+)
+
+// Config is the config for videostore.
+type Config struct {
+	Camera  *string `json:"camera,omitempty"`
+	Storage Storage `json:"storage"`
+}
+
+// Storage is the storage subconfig for videostore.
+type Storage struct {
+	SizeGB      int    `json:"size_gb"`
+	UploadPath  string `json:"upload_path,omitempty"`
+	StoragePath string `json:"storage_path,omitempty"`
+}
path: videostore/config.go
user: login: randhid
body: Double check: these are the same as previous video store's? And in the same json structure?
created_at: 2025-03-11T20:36:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+package videostore
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/utils"
+)
+
+// Config is the config for videostore.
+type Config struct {
+	Camera  *string `json:"camera,omitempty"`
+	Storage Storage `json:"storage"`
+}
+
+// Storage is the storage subconfig for videostore.
+type Storage struct {
+	SizeGB      int    `json:"size_gb"`
+	UploadPath  string `json:"upload_path,omitempty"`
+	StoragePath string `json:"storage_path,omitempty"`
+}
path: videostore/config.go
user: login: nicksanford
body: correct. viam:viamrtsp:video-store has a subset of the config as the video-store module's model
created_at: 2025-03-11T20:38:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,573 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return fmt.Errorf("resource %s unable to be found in viamrtsp registry err: %s", m.camName.String(), err.Error())
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
path: videostore/mux.go
user: login: randhid
body: I don't know how we'd get into this state in this particular - but usually we also check that the worker is nil before stopping it.
created_at: 2025-03-11T21:12:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,573 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return fmt.Errorf("resource %s unable to be found in viamrtsp registry err: %s", m.camName.String(), err.Error())
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
+	return nil
+}
+
+func (m *rawSegmenterMux) Start(codec videostore.CodecType, au [][]byte) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if vsCodec := videostore.CodecType(m.codec.Load()); vsCodec != videostore.CodecTypeUnknown {
+		return fmt.Errorf("init called when codec already set to %s", vsCodec)
+	}
+	switch codec {
+	case videostore.CodecTypeH264:
+		for _, nalu := range au {
+			if len(nalu) == 0 {
+				continue
+			}
+			//nolint:mnd
+			typ := h264.NALUType(nalu[0] & 0x1F)
+			switch typ {
+			case h264.NALUTypeSPS:
+				m.metadata.sps = nalu
+			case h264.NALUTypePPS:
+				m.metadata.pps = nalu
path: videostore/mux.go
user: login: randhid
body: TODO: write what these codecs are 
created_at: 2025-03-11T21:12:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,573 @@
+package videostore
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
+	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
+	"github.com/viam-modules/viamrtsp/registry"
+	"github.com/viam-modules/video-store/videostore"
+	"go.viam.com/rdk/logging"
+	"go.viam.com/rdk/resource"
+	"go.viam.com/utils"
+)
+
+const monitorInterval = time.Second * 5
+
+type metadata struct {
+	width        int
+	height       int
+	vps          []byte
+	sps          []byte
+	pps          []byte
+	dtsExtractor interface {
+		Extract(au [][]byte, pts int64) (int64, error)
+	}
+	spsUnChanged       bool
+	firstTimeStampsSet bool
+	firstPTS           int64
+	firstDTS           int64
+}
+type rawSegmenterMux struct {
+	// are valid for the lifetime of the rawSegmenterMux
+	camName resource.Name
+	logger  logging.Logger
+	worker  *utils.StoppableWorkers
+	regDone <-chan struct{}
+	cam     registry.ModuleCamera
+
+	mu       sync.Mutex
+	rawSeg   *videostore.RawSegmenter
+	codec    atomic.Int64
+	metadata metadata
+}
+
+var codecs = []videostore.CodecType{
+	videostore.CodecTypeH265,
+	videostore.CodecTypeH264,
+}
+
+func newRawSegmenterMux(rawSeg *videostore.RawSegmenter, camName resource.Name, logger logging.Logger) *rawSegmenterMux {
+	return &rawSegmenterMux{
+		rawSeg:  rawSeg,
+		camName: camName,
+		worker:  utils.NewBackgroundStoppableWorkers(),
+		logger:  logger,
+	}
+}
+
+// init and close are called by videostore.
+func (m *rawSegmenterMux) init() error {
+	if m.rawSeg == nil {
+		return errors.New("videostore.RTPVideoStore.Segmenter() is nil")
+	}
+	cam, err := registry.Global.Get(m.camName.String())
+	if err != nil {
+		return fmt.Errorf("resource %s unable to be found in viamrtsp registry err: %s", m.camName.String(), err.Error())
+	}
+	regCtx, err := cam.RequestVideo(m, codecs)
+	if err != nil {
+		return err
+	}
+	m.regDone = regCtx.Done()
+	m.cam = cam
+	m.worker.Add(m.registrationMonitor)
+	return nil
+}
+
+func (m *rawSegmenterMux) registrationMonitor(ctx context.Context) {
+	registered := true
+	timer := time.NewTimer(monitorInterval)
+	defer timer.Stop()
+	defer m.cleanup()
+	for {
+		if err := ctx.Err(); err != nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-m.regDone:
+			registered = false
+			// set the channel to nil so we don't go down this case statement next iteration
+			m.regDone = nil
+
+		case <-timer.C:
+			if !registered {
+				cam, err := registry.Global.Get(m.camName.String())
+				if err != nil {
+					m.logger.Warnf("failed to find camera %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+
+				regCtx, err := cam.RequestVideo(m, codecs)
+				if err != nil {
+					m.logger.Warnf("failed to register video-store with viamrtsp camera, err: %s", err.Error())
+					timer.Reset(monitorInterval)
+					continue
+				}
+				m.regDone = regCtx.Done()
+				m.cam = cam
+				registered = true
+				timer.Reset(monitorInterval)
+				continue
+			}
+
+			if videostore.CodecType(m.codec.Load()) == videostore.CodecTypeUnknown {
+				m.logger.Warn("waiting for viamrtsp camera to send video data to video-store")
+			}
+			timer.Reset(monitorInterval)
+		}
+	}
+}
+
+func (m *rawSegmenterMux) cleanup() {
+	if err := m.Stop(); err != nil {
+		m.logger.Warnf("failed to stop raw segmenter %s", err.Error())
+	}
+	if err := m.cam.CancelRequest(m); err != nil {
+		m.logger.Warnf("DeRegister video-store from viamrtsp camera %s", err.Error())
+	}
+}
+
+func (m *rawSegmenterMux) close() error {
+	if m == nil {
+		return nil
+	}
+	m.worker.Stop()
path: videostore/mux.go
user: login: nicksanford
body: the worker is alwas not nil
created_at: 2025-03-11T22:05:27Z
author_association: CONTRIBUTOR

PR comments:
user: login: seanavery
created_at: 2025-03-11T18:17:27Z
body: @nicksanford Would you mind providing example config to test this out?
author_association: COLLABORATOR
user: login: nicksanford
created_at: 2025-03-11T18:18:50Z
body: > @nicksanford Would you mind providing example config to test this out?

added to the pr descriptions
author_association: CONTRIBUTOR
user: login: seanavery
created_at: 2025-03-11T19:29:12Z
body: @nicksanford Is it ok to have multiple `viam:viamrtsp:video-store` model components pointed at the same camera?
author_association: COLLABORATOR
user: login: nicksanford
created_at: 2025-03-11T19:32:14Z
body: > @nicksanford Is it ok to have multiple `viam:viamrtsp:video-store` model components pointed at the same camera?

It is not allowed, but it is a condition I guard against. 
author_association: CONTRIBUTOR
user: login: nicksanford
created_at: 2025-03-11T19:33:02Z
body: > > @nicksanford Is it ok to have multiple `viam:viamrtsp:video-store` model components pointed at the same camera?
> 
> It is not allowed, but it is a condition I guard against.

Meaning it shouldn't break anything but the second video-store wont succeed in configuring and will log errors until one or the other video-store are removed.
author_association: CONTRIBUTOR
user: login: seanavery
created_at: 2025-03-11T20:29:33Z
body: Manual Testing:

- XVR ✅ 
  - h265 segments playable ✅ 
  - size change handled ✅ 
  - codec change handled ✅ 
- AONVIFCAMERA-YMA42PIRNAF-EF000000050998FA-url0 ✅ 
  - h265 segments playable ✅ 
- ONVIFICAMERA-F26CIRc3-EF00000005087E34-url0 ✅ 
  - h265 segments playable ✅ 
author_association: COLLABORATOR