From a535d7bdfc7690e5b5e02f213ff295840e1cb565 Mon Sep 17 00:00:00 2001 From: Sean Pollock Date: Mon, 13 Jan 2025 21:27:58 +0100 Subject: [PATCH] [RSDK-9653, RSDK-9673] - Make resilient to unknown and changing frame sizes (#33) * Live reinit working * Code cleanup * Lint * Update attr documentation * Remove libjpeg dep * Update tests * Fix keyframe handling across resize boundaries * Add some comments * More cleanup * Do not allow negative framerates * Remove codec from constructor * reinit to frameDimsChanged * Use struct for encoded result * Prevent side effects from partial encoder initialization * Flag encoder to reinit if segmenter init fails * Use debug logs to avoid spam * Fail validation for negative framerate * Move to validate * Explicit encodeResult init --- Makefile | 2 +- README.md | 17 +---- cam/cam.go | 88 ++++++++++--------------- cam/encoder.go | 152 +++++++++++++++++++++++++++++-------------- cam/segmenter.go | 72 +++++++++++++------- tests/config_test.go | 28 ++------ tests/fetch_test.go | 6 +- tests/save_test.go | 6 +- 8 files changed, 196 insertions(+), 175 deletions(-) diff --git a/Makefile b/Makefile index dcf6f4e..9288a28 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ FFMPEG_OPTS ?= --prefix=$(FFMPEG_BUILD) \ CGO_LDFLAGS := -L$(FFMPEG_BUILD)/lib -lavcodec -lavutil -lavformat -lz ifeq ($(SOURCE_OS),linux) - CGO_LDFLAGS += -l:libjpeg.a -l:libx264.a + CGO_LDFLAGS += -l:libx264.a endif ifeq ($(SOURCE_OS),darwin) CGO_LDFLAGS += $(HOMEBREW_PREFIX)/Cellar/x264/r3108/lib/libx264.a -liconv diff --git a/README.md b/README.md index 2b4ceab..02dea84 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,6 @@ On the new component panel, copy and paste the following attribute template into "storage": { "segment_seconds": , "size_gb": , - }, - "cam_props": { - "width": , - "height": , - "framerate": } } ``` @@ -70,13 +65,10 @@ Additionally, make sure to add your configured data manager service to the `depe | | `upload_path` | string | optional | Custom path to use for uploading files. If not under `~/.viam/capture`, you will need to add to `additional_sync_paths` in datamanager service configuration. | | `video` | | object | optional | | | | `format` | string | optional | Name of video format to use (e.g., mp4). | -| | `codec` | string | optional | Name of video codec to use (e.g., h264). | +| | `codec` | string | optional | Name of video codec to use (e.g., h264). | | | `bitrate` | integer | optional | Throughput of encoder in bits per second. Higher for better quality video, and lower for better storage efficiency. | | | `preset` | string | optional | Name of codec video preset to use. See [here](https://trac.ffmpeg.org/wiki/Encode/H.264#a2.Chooseapresetandtune) for preset options. | -| `cam_props` | | object | optional | | -| | `width` | integer | optional | Width of the source camera frames in pixels. If unspecified, will try to autodetect by fetching a frame from the source camera. | -| | `height` | integer | optional | Height of the source camera frames in pixels. If unspecified, will try to autodetect by fetching a frame from the source camera. | -| | `framerate` | integer | optional | Number of frames per second provided by the source camera. Default is 20. | +| `framerate` | | integer | optional | Frame rate of the video in frames per second. Default value is 20 if not set. | ### Example Configuration @@ -92,11 +84,6 @@ Additionally, make sure to add your configured data manager service to the `depe "storage": { "segment_seconds": 10, "size_gb": 50, - }, - "cam_props": { - "width": 640, - "height": 480, - "framerate": 25 } }, "depends_on": [ diff --git a/cam/cam.go b/cam/cam.go index a0c0f70..980c1bc 100644 --- a/cam/cam.go +++ b/cam/cam.go @@ -55,6 +55,7 @@ type videostore struct { cam camera.Camera latestFrame atomic.Pointer[image.Image] workers *utils.StoppableWorkers + framerate int enc *encoder seg *segmenter @@ -78,21 +79,13 @@ type video struct { Format string `json:"format,omitempty"` } -type cameraProperties struct { - Width int `json:"width"` - Height int `json:"height"` - Framerate int `json:"framerate"` -} - // Config is the configuration for the video storage camera component. type Config struct { - Camera string `json:"camera"` - Sync string `json:"sync"` - Storage storage `json:"storage"` - Video video `json:"video,omitempty"` - - // TODO(seanp): Remove once camera properties are returned from camera component. - Properties cameraProperties `json:"cam_props"` + Camera string `json:"camera"` + Sync string `json:"sync"` + Storage storage `json:"storage"` + Video video `json:"video,omitempty"` + Framerate int `json:"framerate,omitempty"` } // Validate validates the configuration for the video storage camera component. @@ -109,6 +102,9 @@ func (cfg *Config) Validate(path string) ([]string, error) { if cfg.Sync == "" { return nil, utils.NewConfigValidationFieldRequiredError(path, "sync") } + if cfg.Framerate < 0 { + return nil, fmt.Errorf("invalid framerate %d, must be greater than 0", cfg.Framerate) + } return []string{cfg.Camera}, nil } @@ -123,7 +119,7 @@ func init() { } func newvideostore( - ctx context.Context, + _ context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, @@ -150,11 +146,11 @@ func newvideostore( ffmppegLogLevel(logLevel) // Create encoder to handle encoding of frames. - // TODO(seanp): Forcing h264 for now until h265 is supported. - codec := defaultVideoCodec + // TODO(seanp): Ignoring codec and using h264 for now until h265 is supported. bitrate := defaultVideoBitrate preset := defaultVideoPreset format := defaultVideoFormat + vs.framerate = defaultFramerate if newConf.Video.Bitrate != 0 { bitrate = newConf.Video.Bitrate } @@ -164,39 +160,15 @@ func newvideostore( if newConf.Video.Format != "" { format = newConf.Video.Format } - - if newConf.Properties.Width == 0 && newConf.Properties.Height == 0 { - vs.logger.Info("received unspecified frame width and height, fetching frame to get dimensions") - for range make([]struct{}, numFetchFrameAttempts) { - frame, err := camera.DecodeImageFromCamera(ctx, rutils.MimeTypeJPEG, nil, vs.cam) - if err != nil { - vs.logger.Warn("failed to get and decode frame from camera, retrying. Error: ", err) - time.Sleep(retryInterval * time.Second) - continue - } - bounds := frame.Bounds() - newConf.Properties.Width = bounds.Dx() - newConf.Properties.Height = bounds.Dy() - vs.logger.Infof("received frame width and height: %d, %d", newConf.Properties.Width, newConf.Properties.Height) - break - } - } - if newConf.Properties.Width == 0 && newConf.Properties.Height == 0 { - return nil, fmt.Errorf("failed to get source camera width and height after %d attempts", numFetchFrameAttempts) - } - - if newConf.Properties.Framerate == 0 { - newConf.Properties.Framerate = defaultFramerate + if newConf.Framerate != 0 { + vs.framerate = newConf.Framerate } vs.enc, err = newEncoder( logger, - codec, bitrate, preset, - newConf.Properties.Width, - newConf.Properties.Height, - newConf.Properties.Framerate, + vs.framerate, ) if err != nil { return nil, err @@ -240,7 +212,6 @@ func newvideostore( vs.storagePath = storagePath vs.seg, err = newSegmenter( logger, - vs.enc, sizeGB, segmentSeconds, storagePath, @@ -359,7 +330,7 @@ func (vs *videostore) Properties(_ context.Context) (camera.Properties, error) { // fetchFrames reads frames from the camera at the framerate interval // and stores the decoded image in the latestFrame atomic pointer. func (vs *videostore) fetchFrames(ctx context.Context) { - frameInterval := time.Second / time.Duration(vs.conf.Properties.Framerate) + frameInterval := time.Second / time.Duration(vs.framerate) ticker := time.NewTicker(frameInterval) defer ticker.Stop() for { @@ -376,7 +347,7 @@ func (vs *videostore) fetchFrames(ctx context.Context) { lazyImage, ok := frame.(*rimage.LazyEncodedImage) if !ok { vs.logger.Error("frame is not of type *rimage.LazyEncodedImage") - return + continue } decodedImage := lazyImage.DecodedImage() vs.latestFrame.Store(&decodedImage) @@ -387,7 +358,7 @@ func (vs *videostore) fetchFrames(ctx context.Context) { // processFrames grabs the latest frame, encodes, and writes to the segmenter // which chunks video stream into clip files inside the storage directory. func (vs *videostore) processFrames(ctx context.Context) { - frameInterval := time.Second / time.Duration(vs.conf.Properties.Framerate) + frameInterval := time.Second / time.Duration(vs.framerate) ticker := time.NewTicker(frameInterval) defer ticker.Stop() for { @@ -400,15 +371,26 @@ func (vs *videostore) processFrames(ctx context.Context) { vs.logger.Debug("latest frame is not available yet") continue } - encoded, pts, dts, err := vs.enc.encode(*latestFrame) + result, err := vs.enc.encode(*latestFrame) if err != nil { - vs.logger.Error("failed to encode frame", err) - return + vs.logger.Debug("failed to encode frame", err) + continue } - err = vs.seg.writeEncodedFrame(encoded, pts, dts) + if result.frameDimsChanged { + vs.logger.Info("reinitializing segmenter due to encoder refresh") + err = vs.seg.initialize(vs.enc.codecCtx) + if err != nil { + vs.logger.Debug("failed to reinitialize segmenter", err) + // Hack that flags the encoder to reinitialize if segmenter fails to + // ensure that encoder and segmenter inits are in sync. + vs.enc.codecCtx = nil + continue + } + } + err = vs.seg.writeEncodedFrame(result.encodedData, result.pts, result.dts) if err != nil { - vs.logger.Error("failed to segment frame", err) - return + vs.logger.Debug("failed to segment frame", err) + continue } } } diff --git a/cam/encoder.go b/cam/encoder.go index 49f81ab..b9ab2ba 100644 --- a/cam/encoder.go +++ b/cam/encoder.go @@ -27,45 +27,87 @@ type encoder struct { codecCtx *C.AVCodecContext srcFrame *C.AVFrame frameCount int64 + framerate int + width int + height int + bitrate int + preset string +} + +type encodeResult struct { + encodedData []byte + pts int64 + dts int64 + frameDimsChanged bool } func newEncoder( logger logging.Logger, - videoCodec codecType, bitrate int, preset string, - width int, - height int, framerate int, ) (*encoder, error) { + // Initialize without codec context and source frame. We will spin up + // the codec context and source frame when we get the first frame or when + // a resize is needed. enc := &encoder{ logger: logger, + codecCtx: nil, + srcFrame: nil, + bitrate: bitrate, + framerate: framerate, + width: 0, + height: 0, frameCount: 0, + preset: preset, + } + + return enc, nil +} + +func (e *encoder) initialize(width, height int) (err error) { + if e.codecCtx != nil { + C.avcodec_close(e.codecCtx) + C.avcodec_free_context(&e.codecCtx) + // We need to reset the frame count when reinitializing the encoder + // in order to ensure that keyframes intervals are generated correctly. + e.frameCount = 0 + } + if e.srcFrame != nil { + C.av_frame_free(&e.srcFrame) } - codecID := lookupCodecIDByType(videoCodec) + // Defer cleanup in case of error. This will prevent side effects from + // a partially initialized encoder. + defer func() { + if err != nil { + if e.codecCtx != nil { + C.avcodec_free_context(&e.codecCtx) + } + if e.srcFrame != nil { + C.av_frame_free(&e.srcFrame) + } + } + }() + codecID := lookupCodecIDByType(codecH264) codec := C.avcodec_find_encoder(codecID) if codec == nil { - return nil, errors.New("codec not found") + return errors.New("codec not found") } - - enc.codecCtx = C.avcodec_alloc_context3(codec) - if enc.codecCtx == nil { - return nil, errors.New("failed to allocate codec context") + e.codecCtx = C.avcodec_alloc_context3(codec) + if e.codecCtx == nil { + return errors.New("failed to allocate codec context") } - - enc.codecCtx.bit_rate = C.int64_t(bitrate) - enc.codecCtx.pix_fmt = C.AV_PIX_FMT_YUV422P - enc.codecCtx.time_base = C.AVRational{num: 1, den: C.int(framerate)} - enc.codecCtx.width = C.int(width) - enc.codecCtx.height = C.int(height) - + e.codecCtx.bit_rate = C.int64_t(e.bitrate) + e.codecCtx.pix_fmt = C.AV_PIX_FMT_YUV422P + e.codecCtx.time_base = C.AVRational{num: 1, den: C.int(e.framerate)} + e.codecCtx.width = C.int(width) + e.codecCtx.height = C.int(height) // TODO(seanp): Do we want b frames? This could make it more complicated to split clips. - enc.codecCtx.max_b_frames = 0 - presetCStr := C.CString(preset) + e.codecCtx.max_b_frames = 0 + presetCStr := C.CString(e.preset) tuneCStr := C.CString("zerolatency") defer C.free(unsafe.Pointer(presetCStr)) defer C.free(unsafe.Pointer(tuneCStr)) - // The user can set the preset and tune for the encoder. This affects the // encoding speed and quality. See https://trac.ffmpeg.org/wiki/Encode/H.264 // for more information. @@ -73,29 +115,26 @@ func newEncoder( defer C.av_dict_free(&opts) ret := C.av_dict_set(&opts, C.CString("preset"), presetCStr, 0) if ret < 0 { - return nil, fmt.Errorf("av_dict_set failed: %s", ffmpegError(ret)) + return fmt.Errorf("av_dict_set failed: %s", ffmpegError(ret)) } ret = C.av_dict_set(&opts, C.CString("tune"), tuneCStr, 0) if ret < 0 { - return nil, fmt.Errorf("av_dict_set failed: %s", ffmpegError(ret)) + return fmt.Errorf("av_dict_set failed: %s", ffmpegError(ret)) } - - ret = C.avcodec_open2(enc.codecCtx, codec, &opts) + ret = C.avcodec_open2(e.codecCtx, codec, &opts) if ret < 0 { - return nil, fmt.Errorf("avcodec_open2: %s", ffmpegError(ret)) + return fmt.Errorf("avcodec_open2: %s", ffmpegError(ret)) } - srcFrame := C.av_frame_alloc() if srcFrame == nil { - C.avcodec_close(enc.codecCtx) - return nil, errors.New("could not allocate source frame") + C.avcodec_close(e.codecCtx) + return errors.New("could not allocate source frame") } - srcFrame.width = enc.codecCtx.width - srcFrame.height = enc.codecCtx.height - srcFrame.format = C.int(enc.codecCtx.pix_fmt) - enc.srcFrame = srcFrame - - return enc, nil + srcFrame.width = e.codecCtx.width + srcFrame.height = e.codecCtx.height + srcFrame.format = C.int(e.codecCtx.pix_fmt) + e.srcFrame = srcFrame + return nil } // encode encodes the given frame and returns the encoded data @@ -103,15 +142,30 @@ func newEncoder( // PTS is calculated based on the frame count and source framerate. // If the polling loop is not running at the source framerate, the // PTS will lag behind actual run time. -func (e *encoder) encode(frame image.Image) ([]byte, int64, int64, error) { +func (e *encoder) encode(frame image.Image) (encodeResult, error) { + result := encodeResult{ + encodedData: nil, + pts: 0, + dts: 0, + frameDimsChanged: false, + } + dy, dx := frame.Bounds().Dy(), frame.Bounds().Dx() + if e.codecCtx == nil || dy != int(e.codecCtx.height) || dx != int(e.codecCtx.width) { + e.logger.Infof("Initializing encoder with frame dimensions %dx%d", dx, dy) + err := e.initialize(dx, dy) + if err != nil { + return result, err + } + result.frameDimsChanged = true + } yuv, err := imageToYUV422(frame) if err != nil { - return nil, 0, 0, err + return result, err } - ySize := frame.Bounds().Dx() * frame.Bounds().Dy() - uSize := (frame.Bounds().Dx() / subsampleFactor) * frame.Bounds().Dy() - vSize := (frame.Bounds().Dx() / subsampleFactor) * frame.Bounds().Dy() + ySize := dx * dy + uSize := (dx / subsampleFactor) * dy + vSize := (dx / subsampleFactor) * dy yPlane := C.CBytes(yuv[:ySize]) uPlane := C.CBytes(yuv[ySize : ySize+uSize]) vPlane := C.CBytes(yuv[ySize+uSize : ySize+uSize+vSize]) @@ -121,9 +175,9 @@ func (e *encoder) encode(frame image.Image) ([]byte, int64, int64, error) { e.srcFrame.data[0] = (*C.uint8_t)(yPlane) e.srcFrame.data[1] = (*C.uint8_t)(uPlane) e.srcFrame.data[2] = (*C.uint8_t)(vPlane) - e.srcFrame.linesize[0] = C.int(frame.Bounds().Dx()) - e.srcFrame.linesize[1] = C.int(frame.Bounds().Dx() / subsampleFactor) - e.srcFrame.linesize[2] = C.int(frame.Bounds().Dx() / subsampleFactor) + e.srcFrame.linesize[0] = C.int(dx) + e.srcFrame.linesize[1] = C.int(dx / subsampleFactor) + e.srcFrame.linesize[2] = C.int(dx / subsampleFactor) // Both PTS and DTS times are equal frameCount multiplied by the time_base. // This assumes that the processFrame routine is running at the source framerate. @@ -144,29 +198,29 @@ func (e *encoder) encode(frame image.Image) ([]byte, int64, int64, error) { ret := C.avcodec_send_frame(e.codecCtx, e.srcFrame) if ret < 0 { - return nil, 0, 0, fmt.Errorf("avcodec_send_frame: %s", ffmpegError(ret)) + return result, fmt.Errorf("avcodec_send_frame: %s", ffmpegError(ret)) } pkt := C.av_packet_alloc() if pkt == nil { - return nil, 0, 0, errors.New("could not allocate packet") + return result, errors.New("could not allocate packet") } // Safe to free the packet since we copy later. defer C.av_packet_free(&pkt) ret = C.avcodec_receive_packet(e.codecCtx, pkt) if ret < 0 { - return nil, 0, 0, fmt.Errorf("avcodec_receive_packet failed %s", ffmpegError(ret)) + return result, fmt.Errorf("avcodec_receive_packet failed %s", ffmpegError(ret)) } // Convert the encoded data to a Go byte slice. This is a necessary copy // to prevent dangling pointer in C memory. By copying to a Go bytes we can // allow the frame to be garbage collected automatically. - encodedData := C.GoBytes(unsafe.Pointer(pkt.data), pkt.size) - pts := int64(pkt.pts) - dts := int64(pkt.dts) + result.encodedData = C.GoBytes(unsafe.Pointer(pkt.data), pkt.size) + result.pts = int64(pkt.pts) + result.dts = int64(pkt.dts) e.frameCount++ - // return encoded data - return encodedData, pts, dts, nil + // return encoded data + return result, nil } func (e *encoder) close() { diff --git a/cam/segmenter.go b/cam/segmenter.go index 38577f0..993e7b8 100644 --- a/cam/segmenter.go +++ b/cam/segmenter.go @@ -29,64 +29,83 @@ type segmenter struct { logger logging.Logger outCtx *C.AVFormatContext stream *C.AVStream - encoder *encoder frameCount int64 maxStorageSize int64 storagePath string + clipLength int + format string } func newSegmenter( logger logging.Logger, - enc *encoder, storageSize int, clipLength int, storagePath string, format string, ) (*segmenter, error) { + // Initialize struct without stream or output context. We will initialize + // these when we get the first frame or when a resize is needed. s := &segmenter{ - logger: logger, - encoder: enc, + logger: logger, + outCtx: nil, + stream: nil, + frameCount: 0, + maxStorageSize: int64(storageSize) * gigabyte, + clipLength: clipLength, + storagePath: storagePath, + format: format, } - s.maxStorageSize = int64(storageSize) * gigabyte - - s.storagePath = storagePath err := createDir(s.storagePath) if err != nil { return nil, err } - outputPatternCStr := C.CString(storagePath + "/" + outputPattern) - defer C.free(unsafe.Pointer(outputPatternCStr)) + + return s, nil +} + +// initialize takes in a codec ctx and initializes the segmenter with the codec parameters. +func (s *segmenter) initialize(codecCtx *C.AVCodecContext) error { + if s.outCtx != nil { + ret := C.av_write_trailer(s.outCtx) + if ret < 0 { + s.logger.Errorf("failed to write trailer", "error", ffmpegError(ret)) + } + // This will also free the stream + C.avformat_free_context(s.outCtx) + } // Allocate output context for segmenter. The "segment" format is a special format // that allows for segmenting output files. The output pattern is a strftime pattern // that specifies the output file name. The pattern is set to the current time. + outputPatternCStr := C.CString(s.storagePath + "/" + outputPattern) + defer C.free(unsafe.Pointer(outputPatternCStr)) var fmtCtx *C.AVFormatContext ret := C.avformat_alloc_output_context2(&fmtCtx, nil, C.CString("segment"), outputPatternCStr) if ret < 0 { - return nil, fmt.Errorf("failed to allocate output context: %s", ffmpegError(ret)) + return fmt.Errorf("failed to allocate output context: %s", ffmpegError(ret)) } stream := C.avformat_new_stream(fmtCtx, nil) if stream == nil { - return nil, errors.New("failed to allocate stream") + return errors.New("failed to allocate stream") } stream.id = C.int(fmtCtx.nb_streams) - 1 - stream.time_base = enc.codecCtx.time_base + stream.time_base = codecCtx.time_base // Copy codec parameters from encoder to segment stream. This is equivalent to // -c:v copy in ffmpeg cli codecpar := C.avcodec_parameters_alloc() defer C.avcodec_parameters_free(&codecpar) - if ret := C.avcodec_parameters_from_context(codecpar, enc.codecCtx); ret < 0 { - return nil, fmt.Errorf("failed to copy codec parameters: %s", ffmpegError(ret)) + if ret := C.avcodec_parameters_from_context(codecpar, codecCtx); ret < 0 { + return fmt.Errorf("failed to copy codec parameters: %s", ffmpegError(ret)) } ret = C.avcodec_parameters_copy(stream.codecpar, codecpar) if ret < 0 { - return nil, fmt.Errorf("failed to copy codec parameters %s", ffmpegError(ret)) + return fmt.Errorf("failed to copy codec parameters %s", ffmpegError(ret)) } - segmentLengthCStr := C.CString(strconv.Itoa(clipLength)) - segmentFormatCStr := C.CString(format) + segmentLengthCStr := C.CString(strconv.Itoa(s.clipLength)) + segmentFormatCStr := C.CString(s.format) resetTimestampsCStr := C.CString("1") breakNonKeyFramesCStr := C.CString("1") strftimeCStr := C.CString("1") @@ -104,44 +123,47 @@ func newSegmenter( defer C.av_dict_free(&opts) ret = C.av_dict_set(&opts, C.CString("segment_time"), segmentLengthCStr, 0) if ret < 0 { - return nil, fmt.Errorf("failed to set segment_time: %s", ffmpegError(ret)) + return fmt.Errorf("failed to set segment_time: %s", ffmpegError(ret)) } ret = C.av_dict_set(&opts, C.CString("segment_format"), segmentFormatCStr, 0) if ret < 0 { - return nil, fmt.Errorf("failed to set segment_format: %s", ffmpegError(ret)) + return fmt.Errorf("failed to set segment_format: %s", ffmpegError(ret)) } ret = C.av_dict_set(&opts, C.CString("reset_timestamps"), resetTimestampsCStr, 0) if ret < 0 { - return nil, fmt.Errorf("failed to set reset_timestamps: %s", ffmpegError(ret)) + return fmt.Errorf("failed to set reset_timestamps: %s", ffmpegError(ret)) } // TODO(seanp): Allowing this could cause flakey playback. Remove if not needed. // Or, fix by adding keyframe forces on the encoder side ret = C.av_dict_set(&opts, C.CString("break_non_keyframes"), breakNonKeyFramesCStr, 0) if ret < 0 { - return nil, fmt.Errorf("failed to set break_non_keyframes: %s", ffmpegError(ret)) + return fmt.Errorf("failed to set break_non_keyframes: %s", ffmpegError(ret)) } ret = C.av_dict_set(&opts, C.CString("strftime"), strftimeCStr, 0) if ret < 0 { - return nil, fmt.Errorf("failed to set strftime: %s", ffmpegError(ret)) + return fmt.Errorf("failed to set strftime: %s", ffmpegError(ret)) } ret = C.avformat_write_header(fmtCtx, &opts) if ret < 0 { - return nil, fmt.Errorf("failed to write header: %s", ffmpegError(ret)) + return fmt.Errorf("failed to write header: %s", ffmpegError(ret)) } // Writing header overwrites the time_base, so we need to reset it. // TODO(seanp): Figure out why this is necessary. - stream.time_base = enc.codecCtx.time_base + stream.time_base = codecCtx.time_base stream.id = C.int(fmtCtx.nb_streams) - 1 s.stream = stream s.outCtx = fmtCtx - return s, nil + return nil } // writeEncodedFrame writes an encoded frame to the output segment file. func (s *segmenter) writeEncodedFrame(encodedData []byte, pts, dts int64) error { + if s.outCtx == nil { + return errors.New("segmenter not initialized") + } pkt := C.AVPacket{ data: (*C.uint8_t)(C.CBytes(encodedData)), size: C.int(len(encodedData)), diff --git a/tests/config_test.go b/tests/config_test.go index faca75b..b343e05 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -100,11 +100,7 @@ func TestModuleConfiguration(t *testing.T) { "upload_path": "%s", "storage_path": "%s" }, - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000, @@ -206,11 +202,7 @@ func TestModuleConfiguration(t *testing.T) { "attributes": { "camera": "fake-cam-1", "sync": "data_manager-1", - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000, @@ -272,11 +264,7 @@ func TestModuleConfiguration(t *testing.T) { "upload_path": "/tmp/video-upload", "storage_path": "/tmp/video-storage" }, - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000, @@ -321,7 +309,7 @@ func TestModuleConfiguration(t *testing.T) { ] }`, fullModuleBinPath) - // cam_props NOT specified + // framerate NOT specified config5 := fmt.Sprintf(` { "components": [ @@ -400,11 +388,7 @@ func TestModuleConfiguration(t *testing.T) { "upload_path": "/tmp", "storage_path": "/tmp" }, - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000, @@ -483,7 +467,7 @@ func TestModuleConfiguration(t *testing.T) { test.That(t, err.Error(), test.ShouldContainSubstring, "size_gb") }) - t.Run("No CamProps succeeds with defaults", func(t *testing.T) { + t.Run("No framerate Succeeds", func(t *testing.T) { timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() r, err := setupViamServer(timeoutCtx, config5) diff --git a/tests/fetch_test.go b/tests/fetch_test.go index 53b029f..7a38614 100644 --- a/tests/fetch_test.go +++ b/tests/fetch_test.go @@ -34,11 +34,7 @@ func TestFetchDoCommand(t *testing.T) { "upload_path": "%s", "storage_path": "%s" }, - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000, diff --git a/tests/save_test.go b/tests/save_test.go index 7f65644..e200507 100644 --- a/tests/save_test.go +++ b/tests/save_test.go @@ -35,11 +35,7 @@ func TestSaveDoCommand(t *testing.T) { "upload_path": "%s", "storage_path": "%s" }, - "cam_props": { - "width": 1280, - "height": 720, - "framerate": 30 - }, + "framerate": 30, "video": { "codec": "h264", "bitrate": 1000000,