From b92e29166f31dffdbda8d5257cb489c150fa14e4 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Thu, 14 Apr 2022 18:07:19 -0400 Subject: [PATCH 01/11] Breaking refactor of device and other types; new control commands --- examples/capture/capture.go | 12 +-- examples/device_info/devinfo.go | 49 ++++----- examples/webcam/webcam.go | 20 ++-- v4l2/capability.go | 26 +++-- v4l2/control.go | 42 ++++++++ v4l2/controls.go | 97 ++++++++++++++++++ v4l2/controls_fwht.go | 15 +++ v4l2/controls_h264.go | 135 +++++++++++++++++++++++++ v4l2/controls_mpeg2.go | 34 +++++++ v4l2/controls_vp8.go | 94 +++++++++++++++++ v4l2/crop.go | 18 ---- v4l2/device/device.go | 173 ++++++++++++++++---------------- v4l2/device/list.go | 2 +- v4l2/dimension.go | 27 +++++ v4l2/errors.go | 17 ++-- v4l2/format.go | 2 +- v4l2/format_frameintervals.go | 86 ++++++++++++++++ v4l2/format_framesizes.go | 58 ++++++----- v4l2/{types.go => version.go} | 0 19 files changed, 714 insertions(+), 193 deletions(-) create mode 100644 v4l2/control.go create mode 100644 v4l2/controls.go create mode 100644 v4l2/controls_fwht.go create mode 100644 v4l2/controls_h264.go create mode 100644 v4l2/controls_mpeg2.go create mode 100644 v4l2/controls_vp8.go create mode 100644 v4l2/dimension.go create mode 100644 v4l2/format_frameintervals.go rename v4l2/{types.go => version.go} (100%) diff --git a/examples/capture/capture.go b/examples/capture/capture.go index 5579eb1..710e606 100644 --- a/examples/capture/capture.go +++ b/examples/capture/capture.go @@ -54,29 +54,29 @@ func main() { log.Fatalf("device does not support any of %#v", preferredFmts) } log.Printf("Found preferred fmt: %s", fmtDesc) - frameSizes, err := v4l2.GetFormatFrameSizes(device.GetFileDescriptor(), fmtDesc.PixelFormat) + frameSizes, err := v4l2.GetFormatFrameSizes(device.FileDescriptor(), fmtDesc.PixelFormat) if err!=nil{ log.Fatalf("failed to get framesize info: %s", err) } // select size 640x480 for format - var frmSize v4l2.FrameSize + var frmSize v4l2.FrameSizeEnum for _, size := range frameSizes { - if size.Width == 640 && size.Height == 480 { + if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 { frmSize = size break } } - if frmSize.Width == 0 { + if frmSize.Size.MinWidth == 0 { log.Fatalf("Size 640x480 not supported for fmt: %s", fmtDesc) } // configure device with preferred fmt if err := device.SetPixFormat(v4l2.PixFormat{ - Width: frmSize.Width, - Height: frmSize.Height, + Width: frmSize.Size.MinWidth, + Height: frmSize.Size.MinHeight, PixelFormat: fmtDesc.PixelFormat, Field: v4l2.FieldNone, }); err != nil { diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index cde9f23..b58c7af 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -21,7 +21,7 @@ func main() { flag.Parse() if devList { - if err := listDevices(); err != nil{ + if err := listDevices(); err != nil { log.Fatal(err) } os.Exit(0) @@ -67,22 +67,21 @@ func listDevices() error { } var busInfo, card string - cap, err := dev.GetCapability() - if err != nil { - // is a media device? - if mdi, err := dev.GetMediaInfo(); err == nil { - if mdi.BusInfo != "" { - busInfo = mdi.BusInfo - }else{ - busInfo = "platform: " + mdi.Driver - } - if mdi.Model != "" { - card = mdi.Model - }else{ - card = mdi.Driver - } + cap := dev.Capability() + + // is a media device? + if mdi, err := dev.GetMediaInfo(); err == nil { + if mdi.BusInfo != "" { + busInfo = mdi.BusInfo + } else { + busInfo = "platform: " + mdi.Driver + } + if mdi.Model != "" { + card = mdi.Model + } else { + card = mdi.Driver } - }else{ + } else { busInfo = cap.BusInfo card = cap.Card } @@ -95,16 +94,12 @@ func listDevices() error { fmt.Printf("Device [%s]: %s: %s\n", path, card, busInfo) - } return nil } func printDeviceDriverInfo(dev *device.Device) error { - caps, err := dev.GetCapability() - if err != nil { - return fmt.Errorf("driver info: %w", err) - } + caps := dev.Capability() // print driver info fmt.Println("Device Info:") @@ -196,15 +191,15 @@ func printFormatDesc(dev *device.Device) error { return fmt.Errorf("format desc: %w", err) } fmt.Println("Supported formats:") - for i, desc := range descs{ - frmSizes, err := v4l2.GetFormatFrameSizes(dev.GetFileDescriptor(), desc.PixelFormat) + for i, desc := range descs { + frmSizes, err := v4l2.GetFormatFrameSizes(dev.FileDescriptor(), desc.PixelFormat) if err != nil { return fmt.Errorf("format desc: %w", err) } var sizeStr strings.Builder sizeStr.WriteString("Sizes: ") - for _, size := range frmSizes{ - sizeStr.WriteString(fmt.Sprintf("[%dx%d] ", size.Width, size.Height)) + for _, size := range frmSizes { + sizeStr.WriteString(fmt.Sprintf("[%dx%d] ", size.Size.MinWidth, size.Size.MinHeight)) } fmt.Printf(template, fmt.Sprintf("[%0d] %s", i, desc.Description), sizeStr.String()) } @@ -258,6 +253,6 @@ func printCaptureParam(dev *device.Device) error { fmt.Printf(template, "Capture mode", hiqual) fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.TimePerFrame.Denominator, params.TimePerFrame.Numerator)) - fmt.Printf(template, "Read buffers", fmt.Sprintf("%d",params.ReadBuffers)) + fmt.Printf(template, "Read buffers", fmt.Sprintf("%d", params.ReadBuffers)) return nil -} \ No newline at end of file +} diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index b7afb30..365f8fd 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -54,6 +54,11 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) for frame := range frames { + if len(frame) == 0{ + log.Print("skipping empty frame") + continue + } + // start boundary io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName)) io.WriteString(w, "Content-Type: image/jpeg\n") @@ -63,7 +68,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { switch pixfmt { case v4l2.PixelFmtMJPEG: if _, err := w.Write(frame); err != nil { - log.Printf("failed to write image: %s", err) + log.Printf("failed to write mjpeg image: %s", err) return } case v4l2.PixelFmtYUYV: @@ -73,7 +78,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { continue } if _, err := w.Write(data); err != nil { - log.Printf("failed to write image: %s", err) + log.Printf("failed to write yuyv image: %s", err) return } } @@ -88,6 +93,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { func main() { port := ":9090" devName := "/dev/video0" + frameRate := int(fps) defaultDev, err := device.Open(devName) skipDefault := false if err != nil { @@ -118,11 +124,12 @@ func main() { flag.IntVar(&height, "h", height, "capture height") flag.StringVar(&format, "f", format, "pixel format") flag.StringVar(&port, "p", port, "webcam service port") + flag.IntVar(&frameRate, "r", frameRate, "frames per second (fps)") flag.Parse() // close device used for default info if err := defaultDev.Close(); err != nil { - // default device failed to close + log.Fatalf("failed to close default device: %s", err) } // open device and setup device @@ -131,10 +138,7 @@ func main() { log.Fatalf("failed to open device: %s", err) } defer device.Close() - caps, err := device.GetCapability() - if err != nil { - log.Println("failed to get device capabilities:", err) - } + caps := device.Capability() log.Printf("device [%s] opened\n", devName) log.Printf("device info: %s", caps.String()) @@ -161,7 +165,7 @@ func main() { // start capture ctx, cancel := context.WithCancel(context.TODO()) - f, err := device.Capture(ctx, fps) + f, err := device.Capture(ctx, uint32(frameRate)) if err != nil { log.Fatalf("stream capture: %s", err) } diff --git a/v4l2/capability.go b/v4l2/capability.go index f246d86..fff796c 100644 --- a/v4l2/capability.go +++ b/v4l2/capability.go @@ -135,44 +135,52 @@ func GetCapability(fd uintptr) (Capability, error) { }, nil } +// GetCapabilities returns device capabilities if supported +func (c Capability) GetCapabilities() uint32 { + if c.IsDeviceCapabilitiesProvided() { + return c.DeviceCapabilities + } + return c.Capabilities +} + // IsVideoCaptureSupported returns caps & CapVideoCapture func (c Capability) IsVideoCaptureSupported() bool { - return (uint32(c.Capabilities) & CapVideoCapture) != 0 + return c.Capabilities & CapVideoCapture != 0 } // IsVideoOutputSupported returns caps & CapVideoOutput func (c Capability) IsVideoOutputSupported() bool { - return (uint32(c.Capabilities) & CapVideoOutput) != 0 + return c.Capabilities & CapVideoOutput != 0 } // IsVideoOverlaySupported returns caps & CapVideoOverlay func (c Capability) IsVideoOverlaySupported() bool { - return (uint32(c.Capabilities) & CapVideoOverlay) != 0 + return c.Capabilities & CapVideoOverlay != 0 } // IsVideoOutputOverlaySupported returns caps & CapVideoOutputOverlay func (c Capability) IsVideoOutputOverlaySupported() bool { - return (uint32(c.Capabilities) & CapVideoOutputOverlay) != 0 + return c.Capabilities & CapVideoOutputOverlay != 0 } // IsVideoCaptureMultiplanarSupported returns caps & CapVideoCaptureMPlane func (c Capability) IsVideoCaptureMultiplanarSupported() bool { - return (uint32(c.Capabilities) & CapVideoCaptureMPlane) != 0 + return c.Capabilities & CapVideoCaptureMPlane != 0 } // IsVideoOutputMultiplanerSupported returns caps & CapVideoOutputMPlane func (c Capability) IsVideoOutputMultiplanerSupported() bool { - return (uint32(c.Capabilities) & CapVideoOutputMPlane) != 0 + return c.Capabilities & CapVideoOutputMPlane != 0 } // IsReadWriteSupported returns caps & CapReadWrite func (c Capability) IsReadWriteSupported() bool { - return (uint32(c.Capabilities) & CapReadWrite) != 0 + return c.Capabilities & CapReadWrite != 0 } // IsStreamingSupported returns caps & CapStreaming func (c Capability) IsStreamingSupported() bool { - return (uint32(c.Capabilities) & CapStreaming) != 0 + return c.Capabilities & CapStreaming != 0 } // IsDeviceCapabilitiesProvided returns true if the device returns @@ -180,7 +188,7 @@ func (c Capability) IsStreamingSupported() bool { // See notes on VL42_CAP_DEVICE_CAPS: // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-querycap.html?highlight=v4l2_cap_device_caps func (c Capability) IsDeviceCapabilitiesProvided() bool { - return (uint32(c.Capabilities) & CapDeviceCapabilities) != 0 + return c.Capabilities & CapDeviceCapabilities != 0 } // GetDriverCapDescriptions return textual descriptions of driver capabilities diff --git a/v4l2/control.go b/v4l2/control.go new file mode 100644 index 0000000..0cb4074 --- /dev/null +++ b/v4l2/control.go @@ -0,0 +1,42 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// Control (v4l2_control) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1725 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ctrl.html +type Control struct { + ID uint32 + Value uint32 +} + +func GetControl(fd uintptr, id uint32) (Control, error) { + var ctrl C.struct_v4l2_control + ctrl.id = C.uint(id) + + if err := send(fd, C.VIDIOC_G_CTRL, uintptr(unsafe.Pointer(&ctrl))); err != nil { + return Control{}, fmt.Errorf("get control: id %d: %w", id, err) + } + + return Control{ + ID: uint32(ctrl.id), + Value: uint32(ctrl.value), + }, nil +} + +func SetControl(fd uintptr, id, value uint32) error { + var ctrl C.struct_v4l2_control + ctrl.id = C.uint(id) + ctrl.value = C.int(value) + + if err := send(fd, C.VIDIOC_G_CTRL, uintptr(unsafe.Pointer(&ctrl))); err != nil { + return fmt.Errorf("set control: id %d: value: %d: %w", id, value, err) + } + + return nil +} diff --git a/v4l2/controls.go b/v4l2/controls.go new file mode 100644 index 0000000..bfe3739 --- /dev/null +++ b/v4l2/controls.go @@ -0,0 +1,97 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// TODO - Implementation of extended controls (v4l2_ext_control) is paused for now, +// so that efforts can be focused on other parts of the API. This can resumed +// later when type v4l2_ext_control and v4l2_ext_controls are better understood. + +// ExtControl (v4l2_ext_control) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1730 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ext-ctrls.html +type ExtControl struct { + ID uint32 + Size uint32 + Ctrl ExtControlUnion +} + +type ExtControlUnion struct { + Value int32 + Value64 int64 + String string + PU8 uint8 + PU16 uint16 + PU32 uint32 + PArea Area + PH264SPS ControlH264SPS + PH264PPS ControlH264PPS + PH264ScalingMatrix ControlH264ScalingMatrix + H264PredWeights ControlH264PredictionWeights + PH264SliceParams ControlH264SliceParams + PH264DecodeParams ControlH264DecodeParams + PFWHTParams ControlFWHTParams + PVP8Frame ControlVP8Frame + PMPEG2Sequence ControlMPEG2Sequence + PMPEG2Picture ControlMPEG2Picture + PMPEG2Quantization ControlMPEG2Quantization + _ uintptr +} + +// ExtControls (v4l2_ext_controls) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1757 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-g-ext-ctrls.html +type ExtControls struct { + Which uint32 + Count uint32 + ErrorIndex uint32 + Controls []ExtControl +} + +// GetExtControls retrieve one or more controls +func GetExtControls(fd uintptr, controls []ExtControl) (ExtControls, error) { + if true { + // TODO remove when supported + return ExtControls{}, fmt.Errorf("unsupported") + } + + var ctrls C.struct_v4l2_ext_controls + ctrls.count = C.uint(len(controls)) + + // prepare control requests + var Cctrls []C.struct_v4l2_ext_control + for _, control := range controls{ + var Cctrl C.struct_v4l2_ext_control + Cctrl.id = C.uint(control.ID) + Cctrl.size = C.uint(control.Size) + *(*ExtControlUnion)(unsafe.Pointer(&Cctrl.anon0[0])) = control.Ctrl + Cctrls = append(Cctrls, Cctrl) + } + ctrls.controls = (*C.struct_v4l2_ext_control)(unsafe.Pointer(&ctrls.controls)) + + if err := send(fd, C.VIDIOC_G_EXT_CTRLS, uintptr(unsafe.Pointer(&ctrls))); err != nil { + return ExtControls{}, fmt.Errorf("get ext controls: %w", err) + } + + // gather returned controls + retCtrls := ExtControls{ + Count: uint32(ctrls.count), + ErrorIndex: uint32(ctrls.error_idx), + } + // extract controls array + Cctrls = *(*[]C.struct_v4l2_ext_control)(unsafe.Pointer(&ctrls.controls)) + for _, Cctrl := range Cctrls { + extCtrl := ExtControl{ + ID: uint32(Cctrl.id), + Size: uint32(Cctrl.size), + Ctrl: *(*ExtControlUnion)(unsafe.Pointer(&Cctrl.anon0[0])), + } + retCtrls.Controls = append(retCtrls.Controls, extCtrl) + } + + return retCtrls, nil +} \ No newline at end of file diff --git a/v4l2/controls_fwht.go b/v4l2/controls_fwht.go new file mode 100644 index 0000000..479b722 --- /dev/null +++ b/v4l2/controls_fwht.go @@ -0,0 +1,15 @@ +package v4l2 + +// ControlFWHTParams (v4l2_ctrl_fwht_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1659 +type ControlFWHTParams struct { + BackwardRefTimestamp uint64 + Version uint32 + Width uint32 + Height uint32 + Flags uint32 + Colorspace ColorspaceType + XFerFunc XferFunctionType + YCbCrEncoding YCbCrEncodingType + Quantization QuantizationType +} diff --git a/v4l2/controls_h264.go b/v4l2/controls_h264.go new file mode 100644 index 0000000..25ffb04 --- /dev/null +++ b/v4l2/controls_h264.go @@ -0,0 +1,135 @@ +package v4l2 + +import "C" + +// TODO - Need to figure out how to import the proper header files for H264 support +const ( + H264NumDPBEntries uint32 = 16 // C.V4L2_H264_NUM_DPB_ENTRIES + H264RefListLength uint32 = 32 // C.V4L2_H264_REF_LIST_LEN +) + +// ControlH264SPS (v4l2_ctrl_h264_sps) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1308 +type ControlH264SPS struct { + ProfileIDC uint8 + ConstraintSetFlags uint8 + LevelIDC uint8 + SequenceParameterSetID uint8 + ChromaFormatIDC uint8 + BitDepthLumaMinus8 uint8 + BitDepthChromaMinus8 uint8 + Log2MaxFrameNumMinus4 uint8 + PicOrderCntType uint8 + Log2MaxPicOrderCntLsbMinus4 uint8 + MaxNumRefFrames uint8 + NumRefFramesInPicOrderCntCycle uint8 + OffsetForRefFrame [255]int32 + OffsetForNonRefPic int32 + OffsetForTopToBottomField int32 + PicWidthInMbsMinus1 uint16 + PicHeightInMapUnitsMinus1 uint16 + Falgs uint32 +} + +// ControlH264PPS (v4l2_ctrl_h264_pps) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1364 +type ControlH264PPS struct { + PicParameterSetID uint8 + SeqParameterSetID uint8 + NumSliceGroupsMinus1 uint8 + NumRefIndexL0DefaultActiveMinus1 uint8 + NumRefIndexL1DefaultActiveMinus1 uint8 + WeightedBipredIDC uint8 + PicInitQPMinus26 int8 + PicInitQSMinus26 int8 + ChromaQPIndexOffset int8 + SecondChromaQPIndexOffset int8 + Flags uint16 +} + +// ControlH264ScalingMatrix (v4l2_ctrl_h264_scaling_matrix) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1396 +type ControlH264ScalingMatrix struct { + ScalingList4x4 [6][16]uint8 + ScalingList8x8 [6][64]uint8 +} + +type H264WeightFators struct { + LumaWeight [32]int16 + LumaOffset [32]int16 + ChromaWeight [32][2]int16 + ChromaOffset [32][2]int16 +} + +// ControlH264PredictionWeights (4l2_ctrl_h264_pred_weights) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1426 +type ControlH264PredictionWeights struct { + LumaLog2WeightDenom uint16 + ChromaLog2WeightDenom uint16 + WeightFactors [2]H264WeightFators +} + +// H264Reference (v4l2_h264_reference) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1452 +type H264Reference struct { + Fields uint8 + Index uint8 +} + +// ControlH264SliceParams (v4l2_ctrl_h264_slice_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1499 +type ControlH264SliceParams struct { + HeaderBitSize uint32 + FirstMBInSlice uint32 + SliceType uint8 + ColorPlaneID uint8 + RedundantPicCnt uint8 + CabacInitIDC uint8 + SliceQPDelta int8 + SliceQSDelta int8 + DisableDeblockingFilterIDC uint8 + SliceAlphaC0OffsetDiv2 int8 + SliceBetaOffsetDiv2 int8 + NumRefIdxL0ActiveMinus1 uint8 + NumRefIdxL1ActiveMinus1 uint8 + + _ uint8 // reserved for padding + + RefPicList0 [H264RefListLength]H264Reference + RefPicList1 [H264RefListLength]H264Reference + + Flags uint32 +} + +// H264DPBEntry (v4l2_h264_dpb_entry) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1544 +type H264DPBEntry struct { + ReferenceTS uint64 + PicNum uint32 + FrameNum uint16 + Fields uint8 + _ [8]uint8 // reserved (padding field) + TopFieldOrder int32 + BottomFieldOrderCnt int32 + Flags uint32 +} + +// ControlH264DecodeParams (v4l2_ctrl_h264_decode_params) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1581 +type ControlH264DecodeParams struct { + DPB [H264NumDPBEntries]H264DPBEntry + NalRefIDC uint16 + FrameNum uint16 + TopFieldOrderCnt int32 + BottomFieldOrderCnt int32 + IDRPicID uint16 + PicOrderCntLSB uint16 + DeltaPicOrderCntBottom int32 + DeltaPicOrderCnt0 int32 + DeltaPicOrderCnt1 int32 + DecRefPicMarkingBitSize uint32 + PicOrderCntBitSize uint32 + SliceGroupChangeCycle uint32 + _ uint32 // reserved (padding) + Flags uint32 +} diff --git a/v4l2/controls_mpeg2.go b/v4l2/controls_mpeg2.go new file mode 100644 index 0000000..1026eea --- /dev/null +++ b/v4l2/controls_mpeg2.go @@ -0,0 +1,34 @@ +package v4l2 + +// ControlMPEG2Sequence (v4l2_ctrl_mpeg2_sequence) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1892 +type ControlMPEG2Sequence struct { + HorizontalSize uint16 + VerticalSize uint16 + VBVBufferSize uint32 + ProfileAndLevelIndication uint16 + ChromaFormat uint8 + Flags uint8 +} + +// ControlMPEG2Picture (v4l2_ctrl_mpeg2_picture) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1939 +type ControlMPEG2Picture struct { + BackwardRefTimestamp uint64 + ForwardRefTimestamp uint64 + Flags uint32 + FCode [2][2]uint8 + PictureCodingType uint8 + PictureStructure uint8 + IntraDCPrecision uint8 + _ [5]uint8 // padding +} + +// ControlMPEG2Quantization (v4l2_ctrl_mpeg2_quantisation) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1972 +type ControlMPEG2Quantization struct { + IntraQuantizerMatrix [64]uint8 + NonIntraQuantizerMatrix [64]uint8 + ChromaIntraQuantizerMatrix [64]uint8 + ChromaNonIntraQuantizerMatrix [64]uint8 +} \ No newline at end of file diff --git a/v4l2/controls_vp8.go b/v4l2/controls_vp8.go new file mode 100644 index 0000000..d0e11e1 --- /dev/null +++ b/v4l2/controls_vp8.go @@ -0,0 +1,94 @@ +package v4l2 + +//#include +import "C" + +const ( + VP8CoefficientProbabilityCount uint32 = 11 // C.V4L2_VP8_COEFF_PROB_CNT + VP8MVProbabilityCount uint32 = 19 // C.V4L2_VP8_MV_PROB_CNT +) + +// VP8Segment (v4l2_vp8_segment) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1692 +type VP8Segment struct { + QuantUpdate [4]int8 + LoopFilterUpdate [4]int8 + SegmentProbabilities [3]uint8 + _ uint8 // padding + Flags uint32 +} + +// VP8LoopFilter (v4l2_vp8_loop_filter) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1719 +type VP8LoopFilter struct { + ReferenceFrameDelta int8 + MBModeDelta int8 + SharpnessLevel uint8 + Level uint8 + _ uint16 // padding + Flags uint32 +} + +// VP8Quantization (v4l2_vp8_quantization) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1744 +type VP8Quantization struct { + YACQIndex uint8 + YDCDelta int8 + Y2DCDelta int8 + Y2ACDelta int8 + UVDCDelta int8 + UVACDelta int8 + _ uint16 +} + +// VP8Entropy (v4l2_vp8_entropy) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1771 +type VP8Entropy struct { + CoefficientProbabilities [4][8][3][VP8CoefficientProbabilityCount]uint8 + YModeProbabilities uint8 + UVModeProbabilities uint8 + MVProbabilities [2][VP8MVProbabilityCount]uint8 + _ [3]uint8 // padding +} + +// VP8EntropyCoderState (v4l2_vp8_entropy_coder_state) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1790 +type VP8EntropyCoderState struct { + Range uint8 + Value uint8 + BitCount uint8 + _ uint8 // padding +} + +// ControlVP8Frame (v4l2_ctrl_vp8_frame) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1836 +type ControlVP8Frame struct { + Segment VP8Segment + LoopFilter VP8LoopFilter + Quantization VP8Quantization + Entropy VP8Entropy + EntropyCoderState VP8EntropyCoderState + + Width uint16 + Height uint16 + + HorizontalScale uint8 + VerticalScale uint8 + + Version uint8 + ProbSkipFalse uint8 + PropIntra uint8 + PropLast uint8 + ProbGF uint8 + NumDCTParts uint8 + + FirstPartSize uint32 + FirstPartHeader uint32 + DCTPartSize uint32 + + LastFrameTimestamp uint64 + GoldenFrameTimestamp uint64 + AltFrameTimestamp uint64 + + Flags uint64 +} diff --git a/v4l2/crop.go b/v4l2/crop.go index f750629..d8a1bf4 100644 --- a/v4l2/crop.go +++ b/v4l2/crop.go @@ -8,24 +8,6 @@ import ( "unsafe" ) -// Rect (v4l2_rect) -// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/dev-overlay.html?highlight=v4l2_rect#c.v4l2_rect -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L412 -type Rect struct { - Left int32 - Top int32 - Width uint32 - Height uint32 -} - -// Fract (v4l2_fract) -// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-enumstd.html#c.v4l2_fract -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L419 -type Fract struct { - Numerator uint32 - Denominator uint32 -} - // CropCapability (v4l2_cropcap) // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#c.v4l2_cropcap // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1221 diff --git a/v4l2/device/device.go b/v4l2/device/device.go index b552f25..141e8d6 100644 --- a/v4l2/device/device.go +++ b/v4l2/device/device.go @@ -11,31 +11,65 @@ import ( ) type Device struct { - path string - file *os.File - fd uintptr - cap *v4l2.Capability - cropCap *v4l2.CropCapability - pixFormat v4l2.PixFormat - buffers [][]byte - requestedBuf v4l2.RequestBuffers - streaming bool + path string + file *os.File + fd uintptr + bufType v4l2.BufType + cap v4l2.Capability + cropCap v4l2.CropCapability + selectedFormat v4l2.PixFormat + supportedFormats []v4l2.PixFormat + buffers [][]byte + requestedBuf v4l2.RequestBuffers + streaming bool } // Open creates opens the underlying device at specified path // and returns a *Device or an error if unable to open device. func Open(path string) (*Device, error) { - file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0666) + file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) if err != nil { return nil, fmt.Errorf("device open: %w", err) } - return &Device{path: path, file: file, fd: file.Fd()}, nil + dev := &Device{path: path, file: file, fd: file.Fd()} + + cap, err := v4l2.GetCapability(file.Fd()) + if err != nil { + if err := file.Close(); err != nil { + return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + } + return nil, fmt.Errorf("device %s: capability: %w", path, err) + } + dev.cap = cap + + switch { + case cap.IsVideoCaptureSupported(): + dev.bufType = v4l2.BufTypeVideoCapture + case cap.IsVideoOutputSupported(): + dev.bufType = v4l2.BufTypeVideoOutput + default: + if err := file.Close(); err != nil { + return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + } + return nil, fmt.Errorf("unsupported device type") + } + + cropCap, err := v4l2.GetCropCapability(file.Fd()) + if err != nil { + if err := file.Close(); err != nil { + return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + } + return nil, fmt.Errorf("device %s: crop capability: %w", path, err) + } + dev.cropCap = cropCap + + return dev, nil } // Close closes the underlying device associated with `d` . func (d *Device) Close() error { - if d.streaming{ - if err := d.StopStream(); err != nil{ + if d.streaming { + if err := d.StopStream(); err != nil { return err } } @@ -43,47 +77,33 @@ func (d *Device) Close() error { return d.file.Close() } -// GetFileDescriptor returns the file descriptor value for the device -func (d *Device) GetFileDescriptor() uintptr { +// Name returns the device name (or path) +func (d *Device) Name() uintptr { return d.fd } -// GetCapability retrieves device capability info and -// caches it for future capability check. -func (d *Device) GetCapability() (*v4l2.Capability, error) { - if d.cap != nil { - return d.cap, nil - } - cap, err := v4l2.GetCapability(d.fd) - if err != nil { - return nil, fmt.Errorf("device: %w", err) - } - d.cap = &cap - return d.cap, nil +// FileDescriptor returns the file descriptor value for the device +func (d *Device) FileDescriptor() uintptr { + return d.fd } -// GetCropCapability returns cropping info for device `d` -// and caches it for future capability check. -func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { - if d.cropCap != nil { - return *d.cropCap, nil - } - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.CropCapability{}, fmt.Errorf("device: %w", err) - } +// Capability returns device capability info. +func (d *Device) Capability() v4l2.Capability { + return d.cap +} - cropCap, err := v4l2.GetCropCapability(d.fd) - if err != nil { - return v4l2.CropCapability{}, fmt.Errorf("device: %w", err) +// GetCropCapability returns cropping info for device +func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.CropCapability{}, v4l2.ErrorUnsupportedFeature } - d.cropCap = &cropCap - return cropCap, nil + return d.cropCap, nil } // SetCropRect crops the video dimension for the device func (d *Device) SetCropRect(r v4l2.Rect) error { - if err := d.assertVideoCaptureSupport(); err != nil { - return fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.ErrorUnsupportedFeature } if err := v4l2.SetCropRect(d.fd, r); err != nil { return fmt.Errorf("device: %w", err) @@ -93,67 +113,70 @@ func (d *Device) SetCropRect(r v4l2.Rect) error { // GetPixFormat retrieves pixel format info for device func (d *Device) GetPixFormat() (v4l2.PixFormat, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.PixFormat{}, v4l2.ErrorUnsupportedFeature } pixFmt, err := v4l2.GetPixFormat(d.fd) if err != nil { return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) } + d.selectedFormat = pixFmt return pixFmt, nil } // SetPixFormat sets the pixel format for the associated device. func (d *Device) SetPixFormat(pixFmt v4l2.PixFormat) error { - if err := d.assertVideoCaptureSupport(); err != nil { - return fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.ErrorUnsupportedFeature } if err := v4l2.SetPixFormat(d.fd, pixFmt); err != nil { return fmt.Errorf("device: %w", err) } + d.selectedFormat = pixFmt return nil } // GetFormatDescription returns a format description for the device at specified format index func (d *Device) GetFormatDescription(idx uint32) (v4l2.FormatDescription, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.FormatDescription{}, fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.FormatDescription{}, v4l2.ErrorUnsupportedFeature } return v4l2.GetFormatDescription(d.fd, idx) } - // GetFormatDescriptions returns all possible format descriptions for device func (d *Device) GetFormatDescriptions() ([]v4l2.FormatDescription, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return nil, fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return nil, v4l2.ErrorUnsupportedFeature } return v4l2.GetAllFormatDescriptions(d.fd) } // GetVideoInputIndex returns current video input index for device -func (d *Device) GetVideoInputIndex()(int32, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return 0, fmt.Errorf("device: %w", err) +func (d *Device) GetVideoInputIndex() (int32, error) { + if !d.cap.IsVideoCaptureSupported() { + return 0, v4l2.ErrorUnsupportedFeature } + return v4l2.GetCurrentVideoInputIndex(d.fd) } // GetVideoInputInfo returns video input info for device func (d *Device) GetVideoInputInfo(index uint32) (v4l2.InputInfo, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.InputInfo{}, fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.InputInfo{}, v4l2.ErrorUnsupportedFeature } + return v4l2.GetVideoInputInfo(d.fd, index) } // GetCaptureParam returns streaming capture parameter information func (d *Device) GetCaptureParam() (v4l2.CaptureParam, error) { - if err := d.assertVideoCaptureSupport(); err != nil { - return v4l2.CaptureParam{}, fmt.Errorf("device: %w", err) + if !d.cap.IsVideoCaptureSupported() { + return v4l2.CaptureParam{}, v4l2.ErrorUnsupportedFeature } return v4l2.GetStreamCaptureParam(d.fd) } @@ -167,9 +190,6 @@ func (d *Device) StartStream(buffSize uint32) error { if d.streaming { return nil } - if err := d.assertVideoStreamSupport(); err != nil { - return fmt.Errorf("device: %w", err) - } // allocate device buffers bufReq, err := v4l2.InitBuffers(d.fd, buffSize) @@ -275,7 +295,7 @@ func (d *Device) Capture(ctx context.Context, fps uint32) (<-chan []byte, error) return dataChan, nil } -func (d *Device) StopStream() error{ +func (d *Device) StopStream() error { d.streaming = false for i := 0; i < len(d.buffers); i++ { if err := v4l2.UnmapMemoryBuffer(d.buffers[i]); err != nil { @@ -286,29 +306,4 @@ func (d *Device) StopStream() error{ return fmt.Errorf("device: stop stream: %w", err) } return nil -} - -func (d *Device) assertVideoCaptureSupport() error { - cap, err := d.GetCapability() - if err != nil { - return fmt.Errorf("device capability: %w", err) - } - if !cap.IsVideoCaptureSupported() { - return fmt.Errorf("device capability: video capture not supported") - } - return nil -} - -func (d *Device) assertVideoStreamSupport() error { - cap, err := d.GetCapability() - if err != nil { - return fmt.Errorf("device capability: %w", err) - } - if !cap.IsVideoCaptureSupported() { - return fmt.Errorf("device capability: video capture not supported") - } - if !cap.IsStreamingSupported() { - return fmt.Errorf("device capability: streaming not supported") - } - return nil -} +} \ No newline at end of file diff --git a/v4l2/device/list.go b/v4l2/device/list.go index 0c2a6a5..702f938 100644 --- a/v4l2/device/list.go +++ b/v4l2/device/list.go @@ -10,7 +10,7 @@ var ( root = "/dev" ) -// devPattern is device directory name pattern on Linux +// devPattern is device directory name pattern on Linux (i.e. video0, video10, vbi0, etc) var devPattern = regexp.MustCompile(fmt.Sprintf(`%s/(video|radio|vbi|swradio|v4l-subdev|v4l-touch|media)[0-9]+`, root)) // IsDevice tests whether the path matches a V4L device name and is a device file diff --git a/v4l2/dimension.go b/v4l2/dimension.go new file mode 100644 index 0000000..aed02e6 --- /dev/null +++ b/v4l2/dimension.go @@ -0,0 +1,27 @@ +package v4l2 + +// Area (v4l2_area) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L424 +type Area struct { + Width uint32 + Height uint32 +} + +// Fract (v4l2_fract) +// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/vidioc-enumstd.html#c.v4l2_fract +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L419 +type Fract struct { + Numerator uint32 + Denominator uint32 +} + +// Rect (v4l2_rect) +// https://www.kernel.org/doc/html/v4.14/media/uapi/v4l/dev-overlay.html?highlight=v4l2_rect#c.v4l2_rect +// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L412 +type Rect struct { + Left int32 + Top int32 + Width uint32 + Height uint32 +} + diff --git a/v4l2/errors.go b/v4l2/errors.go index ab100af..7b454a1 100644 --- a/v4l2/errors.go +++ b/v4l2/errors.go @@ -6,21 +6,22 @@ import ( ) var ( - ErrorSystem = errors.New("system error") - ErrorBadArgument = errors.New("bad argument error") - ErrorTemporary = errors.New("temporary error") - ErrorTimeout = errors.New("timeout error") - ErrorUnsupported = errors.New("unsupported error") + ErrorSystem = errors.New("system error") + ErrorBadArgument = errors.New("bad argument error") + ErrorTemporary = errors.New("temporary error") + ErrorTimeout = errors.New("timeout error") + ErrorUnsupported = errors.New("unsupported error") + ErrorUnsupportedFeature = errors.New("feature unsupported error") ) func parseErrorType(errno sys.Errno) error { switch errno { case sys.EBADF, sys.ENOMEM, sys.ENODEV, sys.EIO, sys.ENXIO, sys.EFAULT: // structural, terminal - return ErrorSystem + return ErrorSystem case sys.EINVAL: // bad argument - return ErrorBadArgument + return ErrorBadArgument case sys.ENOTTY: // unsupported - return ErrorUnsupported + return ErrorUnsupported default: if errno.Timeout() { return ErrorTimeout diff --git a/v4l2/format.go b/v4l2/format.go index dab36bf..84f655c 100644 --- a/v4l2/format.go +++ b/v4l2/format.go @@ -301,7 +301,7 @@ func GetPixFormat(fd uintptr) (PixFormat, error) { Priv: uint32(v4l2PixFmt.priv), Flags: uint32(v4l2PixFmt.flags), YcbcrEnc: *(*uint32)(unsafe.Pointer(&v4l2PixFmt.anon0[0])), - HSVEnc: *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2PixFmt.anon0[0])) + unsafe.Sizeof(&v4l2PixFmt.anon0[0]))), + HSVEnc: *(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2PixFmt.anon0[0])) + unsafe.Sizeof(C.uint(0)))), Quantization: uint32(v4l2PixFmt.quantization), XferFunc: uint32(v4l2PixFmt.xfer_func), }, nil diff --git a/v4l2/format_frameintervals.go b/v4l2/format_frameintervals.go new file mode 100644 index 0000000..b9612a4 --- /dev/null +++ b/v4l2/format_frameintervals.go @@ -0,0 +1,86 @@ +package v4l2 + +//#include +import "C" +import ( + "fmt" + "unsafe" +) + +// FrameIntervalType (v4l2_frmivaltypes) +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L845 +type FrameIntervalType = uint32 + +const ( + FrameIntervalTypeDiscrete FrameIntervalType = C.V4L2_FRMIVAL_TYPE_DISCRETE + FrameIntervalTypeContinuous FrameIntervalType = C.V4L2_FRMIVAL_TYPE_CONTINUOUS + FrameIntervalTypeStepwise FrameIntervalType = C.V4L2_FRMIVAL_TYPE_STEPWISE +) + +// FrameIntervalEnum is used to store v4l2_frmivalenum values. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L857 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +type FrameIntervalEnum struct { + Index uint32 + PixelFormat FourCCType + Width uint32 + Height uint32 + Type FrameIntervalType + Interval FrameInterval +} + +// FrameInterval stores all frame interval values regardless of its type. This type maps to v4l2_frmival_stepwise. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L851 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +type FrameInterval struct { + Min Fract + Max Fract + Step Fract +} + +// getFrameInterval retrieves the supported frame interval info from following union based on the type: + +// union { +// struct v4l2_fract discrete; +// struct v4l2_frmival_stepwise stepwise; +// } + +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html +func getFrameInterval(interval C.struct_v4l2_frmivalenum) (FrameIntervalEnum,error) { + frmInterval := FrameIntervalEnum{ + Index: uint32(interval.index), + Type: FrameIntervalType(interval._type), + PixelFormat: FourCCType(interval.pixel_format), + Width: uint32(interval.width), + Height: uint32(interval.height), + } + intervalType := uint32(interval._type) + switch intervalType { + case FrameIntervalTypeDiscrete: + fiDiscrete := *(*Fract)(unsafe.Pointer(&interval.anon0[0])) + frmInterval.Interval.Min = fiDiscrete + frmInterval.Interval.Max = fiDiscrete + frmInterval.Interval.Step.Numerator = 1 + frmInterval.Interval.Step.Denominator = 1 + case FrameIntervalTypeStepwise, FrameIntervalTypeContinuous: + // Calculate pointer to stepwise member of union + frmInterval.Interval = *(*FrameInterval)(unsafe.Pointer(uintptr(unsafe.Pointer(&interval.anon0[0])) + unsafe.Sizeof(Fract{}))) + default: + return FrameIntervalEnum{}, fmt.Errorf("unsupported frame interval type: %d", intervalType) + } + return frmInterval, nil +} + +// GetFormatFrameInterval returns a supported device frame interval for a specified encoding at index and format +func GetFormatFrameInterval(fd uintptr, index uint32, encoding FourCCType, width, height uint32) (FrameIntervalEnum, error) { + var interval C.struct_v4l2_frmivalenum + interval.index = C.uint(index) + interval.pixel_format = C.uint(encoding) + interval.width = C.uint(width) + interval.height = C.uint(height) + + if err := send(fd, C.VIDIOC_ENUM_FRAMEINTERVALS, uintptr(unsafe.Pointer(&interval))); err != nil { + return FrameIntervalEnum{}, fmt.Errorf("frame interval: index %d: %w", index, err) + } + return getFrameInterval(interval) +} diff --git a/v4l2/format_framesizes.go b/v4l2/format_framesizes.go index 45124ee..3f5934f 100644 --- a/v4l2/format_framesizes.go +++ b/v4l2/format_framesizes.go @@ -17,15 +17,15 @@ const ( FrameSizeTypeStepwise FrameSizeType = C.V4L2_FRMSIZE_TYPE_STEPWISE ) -// FrameSize uses v4l2_frmsizeenum to get supporeted frame size for the driver based for the pixel format. +// FrameSizeEnum uses v4l2_frmsizeenum to get supporeted frame size for the driver based for the pixel format. // Use FrameSizeType to determine which sizes the driver support. // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-framesizes.html -type FrameSize struct { - FrameSizeType - FrameSizeDiscrete - FrameSizeStepwise +type FrameSizeEnum struct { + Index uint32 + Type FrameSizeType PixelFormat FourCCType + Size FrameSize } // FrameSizeDiscrete (v4l2_frmsize_discrete) @@ -35,9 +35,10 @@ type FrameSizeDiscrete struct { Height uint32 // height [pixel] } -// FrameSizeStepwise (v4l2_frmsize_stepwise) -// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L820 -type FrameSizeStepwise struct { +// FrameSize stores all possible frame size information regardless of its type. It is mapped to v4l2_frmsize_stepwise. +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L820 +// See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-framesizes.html +type FrameSize struct { MinWidth uint32 // Minimum frame width [pixel] MaxWidth uint32 // Maximum frame width [pixel] StepWidth uint32 // Frame width step size [pixel] @@ -46,40 +47,45 @@ type FrameSizeStepwise struct { StepHeight uint32 // Frame height step size [pixel] } -// getFrameSize retrieves the supported frame size based on the type -func getFrameSize(frmSizeEnum C.struct_v4l2_frmsizeenum) FrameSize { - frameSize := FrameSize{FrameSizeType: FrameSizeType(frmSizeEnum._type), PixelFormat: FourCCType(frmSizeEnum.pixel_format)} - switch frameSize.FrameSizeType { +// getFrameSize retrieves the supported frame size info from following union based on the type: + +// union { +// struct v4l2_frmsize_discrete discrete; +// struct v4l2_frmsize_stepwise stepwise; +// } + +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829 +func getFrameSize(frmSizeEnum C.struct_v4l2_frmsizeenum) FrameSizeEnum { + frameSize := FrameSizeEnum{Type: FrameSizeType(frmSizeEnum._type), PixelFormat: FourCCType(frmSizeEnum.pixel_format)} + switch frameSize.Type { case FrameSizeTypeDiscrete: - fsDiscrete := (*FrameSizeDiscrete)(unsafe.Pointer(&frmSizeEnum.anon0[0])) - frameSize.FrameSizeDiscrete = *fsDiscrete - frameSize.FrameSizeStepwise.MinWidth = frameSize.FrameSizeDiscrete.Width - frameSize.FrameSizeStepwise.MinHeight = frameSize.FrameSizeDiscrete.Height - frameSize.FrameSizeStepwise.MaxWidth = frameSize.FrameSizeDiscrete.Width - frameSize.FrameSizeStepwise.MaxHeight = frameSize.FrameSizeDiscrete.Height + fsDiscrete := *(*FrameSizeDiscrete)(unsafe.Pointer(&frmSizeEnum.anon0[0])) + frameSize.Size.MinWidth = fsDiscrete.Width + frameSize.Size.MinHeight = fsDiscrete.Height + frameSize.Size.MaxWidth = fsDiscrete.Width + frameSize.Size.MaxHeight = fsDiscrete.Height case FrameSizeTypeStepwise, FrameSizeTypeContinuous: - fsStepwise := (*FrameSizeStepwise)(unsafe.Pointer(&frmSizeEnum.anon0[0])) - frameSize.FrameSizeStepwise = *fsStepwise - frameSize.FrameSizeDiscrete.Width = frameSize.FrameSizeStepwise.MaxWidth - frameSize.FrameSizeDiscrete.Height = frameSize.FrameSizeStepwise.MaxHeight + // Calculate pointer to access stepwise member + frameSize.Size = *(*FrameSize)(unsafe.Pointer(uintptr(unsafe.Pointer(&frmSizeEnum.anon0[0]))+unsafe.Sizeof(FrameSizeDiscrete{}))) + default: } return frameSize } // GetFormatFrameSize returns a supported device frame size for a specified encoding at index -func GetFormatFrameSize(fd uintptr, index uint32, encoding FourCCType) (FrameSize, error) { +func GetFormatFrameSize(fd uintptr, index uint32, encoding FourCCType) (FrameSizeEnum, error) { var frmSizeEnum C.struct_v4l2_frmsizeenum frmSizeEnum.index = C.uint(index) frmSizeEnum.pixel_format = C.uint(encoding) if err := send(fd, C.VIDIOC_ENUM_FRAMESIZES, uintptr(unsafe.Pointer(&frmSizeEnum))); err != nil { - return FrameSize{}, fmt.Errorf("frame size: index %d: %w", index, err) + return FrameSizeEnum{}, fmt.Errorf("frame size: index %d: %w", index, err) } return getFrameSize(frmSizeEnum), nil } // GetFormatFrameSizes returns all supported device frame sizes for a specified encoding -func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSize, err error) { +func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSizeEnum, err error) { index := uint32(0) for { var frmSizeEnum C.struct_v4l2_frmsizeenum @@ -107,7 +113,7 @@ func GetFormatFrameSizes(fd uintptr, encoding FourCCType) (result []FrameSize, e // GetAllFormatFrameSizes returns all supported frame sizes for all supported formats. // It iterates from format at index 0 until it encounters and error and then stops. For // each supported format, it retrieves all supported frame sizes. -func GetAllFormatFrameSizes(fd uintptr) (result []FrameSize, err error) { +func GetAllFormatFrameSizes(fd uintptr) (result []FrameSizeEnum, err error) { formats, err := GetAllFormatDescriptions(fd) if len(formats) == 0 && err != nil { return nil, fmt.Errorf("frame sizes: %w", err) diff --git a/v4l2/types.go b/v4l2/version.go similarity index 100% rename from v4l2/types.go rename to v4l2/version.go From 79496a65d6c2d4e6a16fa34bfc50d8e7439e2eb5 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sun, 17 Apr 2022 13:45:54 -0400 Subject: [PATCH 02/11] Support v4l2_streamparam; refactor Device.Open and Device.StartStream --- examples/device_info/devinfo.go | 2 +- v4l2/crop.go | 4 +- v4l2/device/device.go | 36 +++++++++----- v4l2/stream_param.go | 62 ++++++++++++++++++++---- v4l2/streaming.go | 86 ++++++++++++++++----------------- 5 files changed, 123 insertions(+), 67 deletions(-) diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index b58c7af..f1acce3 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -234,7 +234,7 @@ func printCropInfo(dev *device.Device) error { } func printCaptureParam(dev *device.Device) error { - params, err := dev.GetCaptureParam() + params, err := dev.GetStreamParam() if err != nil { return fmt.Errorf("streaming capture param: %w", err) } diff --git a/v4l2/crop.go b/v4l2/crop.go index d8a1bf4..2456472 100644 --- a/v4l2/crop.go +++ b/v4l2/crop.go @@ -21,9 +21,9 @@ type CropCapability struct { // GetCropCapability retrieves cropping info for specified device // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-cropcap.html#ioctl-vidioc-cropcap -func GetCropCapability(fd uintptr) (CropCapability, error) { +func GetCropCapability(fd uintptr, bufType BufType) (CropCapability, error) { var cap C.struct_v4l2_cropcap - cap._type = C.uint(BufTypeVideoCapture) + cap._type = C.uint(bufType) if err := send(fd, C.VIDIOC_CROPCAP, uintptr(unsafe.Pointer(&cap))); err != nil { return CropCapability{}, fmt.Errorf("crop capability: %w", err) diff --git a/v4l2/device/device.go b/v4l2/device/device.go index 141e8d6..d450484 100644 --- a/v4l2/device/device.go +++ b/v4l2/device/device.go @@ -38,7 +38,7 @@ func Open(path string) (*Device, error) { if err := file.Close(); err != nil { return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) } - return nil, fmt.Errorf("device %s: capability: %w", path, err) + return nil, fmt.Errorf("device open: %s: %w", path, err) } dev.cap = cap @@ -49,17 +49,17 @@ func Open(path string) (*Device, error) { dev.bufType = v4l2.BufTypeVideoOutput default: if err := file.Close(); err != nil { - return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + return nil, fmt.Errorf("device open: %s: closing after failure: %s", path, err) } - return nil, fmt.Errorf("unsupported device type") + return nil, fmt.Errorf("device open: %s: %w", path, v4l2.ErrorUnsupportedFeature) } - cropCap, err := v4l2.GetCropCapability(file.Fd()) + cropCap, err := v4l2.GetCropCapability(file.Fd(), dev.bufType) if err != nil { if err := file.Close(); err != nil { - return nil, fmt.Errorf("device %s: closing after failure: %s", path, err) + return nil, fmt.Errorf("device open: %s: closing after failure: %s", path, err) } - return nil, fmt.Errorf("device %s: crop capability: %w", path, err) + return nil, fmt.Errorf("device open: %s: %w", path, err) } dev.cropCap = cropCap @@ -173,12 +173,26 @@ func (d *Device) GetVideoInputInfo(index uint32) (v4l2.InputInfo, error) { return v4l2.GetVideoInputInfo(d.fd, index) } -// GetCaptureParam returns streaming capture parameter information -func (d *Device) GetCaptureParam() (v4l2.CaptureParam, error) { +// GetStreamParam returns streaming parameter information for device +func (d *Device) GetStreamParam() (v4l2.StreamParam, error) { + if !d.cap.IsVideoCaptureSupported() { + return v4l2.StreamParam{}, v4l2.ErrorUnsupportedFeature + } + return v4l2.GetStreamParam(d.fd) +} + +// SetStreamParam saves stream parameters for device +func (d *Device) SetStreamParam() (v4l2.StreamParam, error) { if !d.cap.IsVideoCaptureSupported() { - return v4l2.CaptureParam{}, v4l2.ErrorUnsupportedFeature + return v4l2.StreamParam{}, v4l2.ErrorUnsupportedFeature } - return v4l2.GetStreamCaptureParam(d.fd) + return v4l2.GetStreamParam(d.fd) +} + +// SetCaptureFramerate sets the video capture FPS value of the device +func (d *Device) SetCaptureFramerate (fps uint32) error { + param := v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator:1,Denominator: fps}} + return v4l2.SetStreamParam(d.fd, v4l2.StreamParam{Capture: param}) } // GetMediaInfo returns info for a device that supports the Media API @@ -192,7 +206,7 @@ func (d *Device) StartStream(buffSize uint32) error { } // allocate device buffers - bufReq, err := v4l2.InitBuffers(d.fd, buffSize) + bufReq, err := v4l2.RequestBuffersInfo(d.fd, v4l2.IOTypeMMAP, d.bufType, buffSize) if err != nil { return fmt.Errorf("device: start stream: %w", err) } diff --git a/v4l2/stream_param.go b/v4l2/stream_param.go index 1dd24ce..1302e85 100644 --- a/v4l2/stream_param.go +++ b/v4l2/stream_param.go @@ -18,7 +18,16 @@ const ( StreamParamTimePerFrame StreamParamFlag = C.V4L2_CAP_TIMEPERFRAME ) -// CaptureParam (v4l2_captureparam) +// StreamParam (v4l2_streamparam) +// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_streamparm +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2362 +type StreamParam struct { + Type IOType + Capture CaptureParam + Output OutputParam +} + +// CaptureParam (v4l2_captureparm) // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_captureparm // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1205 type CaptureParam struct { @@ -30,16 +39,49 @@ type CaptureParam struct { _ [4]uint32 } -// GetStreamCaptureParam returns streaming capture parameter for the driver (v4l2_streamparm). -// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html -// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2347 +// OutputParam (v4l2_outputparm) +// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html#c.V4L.v4l2_outputparm +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1228 +type OutputParam struct { + Capability StreamParamFlag + CaptureMode StreamParamFlag + TimePerFrame Fract + ExtendedMode uint32 + WriteBuffers uint32 + _ [4]uint32 +} -func GetStreamCaptureParam(fd uintptr) (CaptureParam, error) { - var param C.struct_v4l2_streamparm - param._type = C.uint(BufTypeVideoCapture) +// GetStreamParam returns streaming parameters for the driver (v4l2_streamparm). +// https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html +// See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2362 +func GetStreamParam(fd uintptr) (StreamParam, error) { + var v4l2Param C.struct_v4l2_streamparm + v4l2Param._type = C.uint(BufTypeVideoCapture) - if err := send(fd, C.VIDIOC_G_PARM, uintptr(unsafe.Pointer(¶m))); err != nil { - return CaptureParam{}, fmt.Errorf("stream param: %w", err) + if err := send(fd, C.VIDIOC_G_PARM, uintptr(unsafe.Pointer(&v4l2Param))); err != nil { + return StreamParam{}, fmt.Errorf("stream param: %w", err) } - return *(*CaptureParam)(unsafe.Pointer(¶m.parm[0])), nil + + capture := *(*CaptureParam)(unsafe.Pointer(&v4l2Param.parm[0])) + output := *(*OutputParam)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Param.parm[0])) + unsafe.Sizeof(C.struct_v4l2_captureparm{}))) + + return StreamParam{ + Type: BufTypeVideoCapture, + Capture: capture, + Output: output, + }, nil } + +func SetStreamParam(fd uintptr, param StreamParam) error { + var v4l2Param C.struct_v4l2_streamparm + v4l2Param._type = C.uint(BufTypeVideoCapture) + *(*C.struct_v4l2_captureparm)(unsafe.Pointer(&v4l2Param.parm[0])) = *(*C.struct_v4l2_captureparm)(unsafe.Pointer(¶m.Capture)) + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Param.parm[0])) + unsafe.Sizeof(C.struct_v4l2_captureparam{}))) = + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(¶m.Output)) + + if err := send(fd, C.VIDIOC_S_PARM, uintptr(unsafe.Pointer(&v4l2Param))); err != nil { + return fmt.Errorf("stream param: %w", err) + } + + return nil +} \ No newline at end of file diff --git a/v4l2/streaming.go b/v4l2/streaming.go index fc7dfe5..5c0fb1b 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -26,16 +26,16 @@ const ( BufTypeOverlay BufType = C.V4L2_BUF_TYPE_VIDEO_OVERLAY ) -// StreamType (v4l2_memory) +// IOType (v4l2_memory) // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/mmap.html?highlight=v4l2_memory_mmap // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L188 -type StreamType = uint32 +type IOType = uint32 const ( - StreamTypeMMAP StreamType = C.V4L2_MEMORY_MMAP - StreamTypeUserPtr StreamType = C.V4L2_MEMORY_USERPTR - StreamTypeOverlay StreamType = C.V4L2_MEMORY_OVERLAY - StreamTypeDMABuf StreamType = C.V4L2_MEMORY_DMABUF + IOTypeMMAP IOType = C.V4L2_MEMORY_MMAP + IOTypeUserPtr IOType = C.V4L2_MEMORY_USERPTR + IOTypeOverlay IOType = C.V4L2_MEMORY_OVERLAY + IOTypeDMABuf IOType = C.V4L2_MEMORY_DMABUF ) // TODO implement vl42_create_buffers @@ -58,37 +58,37 @@ type RequestBuffers struct { // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L1037 // type Buffer struct { - Index uint32 - StreamType uint32 - BytesUsed uint32 - Flags uint32 - Field uint32 - Timestamp sys.Timeval - Timecode Timecode - Sequence uint32 - Memory uint32 - Info BufferInfo // union m - Length uint32 - Reserved2 uint32 - RequestFD int32 + Index uint32 + Type uint32 + BytesUsed uint32 + Flags uint32 + Field uint32 + Timestamp sys.Timeval + Timecode Timecode + Sequence uint32 + Memory uint32 + Info BufferInfo // union m + Length uint32 + Reserved2 uint32 + RequestFD int32 } // makeBuffer makes a Buffer value from C.struct_v4l2_buffer func makeBuffer(v4l2Buf C.struct_v4l2_buffer) Buffer { return Buffer{ - Index: uint32(v4l2Buf.index), - StreamType: uint32(v4l2Buf._type), - BytesUsed: uint32(v4l2Buf.bytesused), - Flags: uint32(v4l2Buf.flags), - Field: uint32(v4l2Buf.field), - Timestamp: *(*sys.Timeval)(unsafe.Pointer(&v4l2Buf.timestamp)), - Timecode: *(*Timecode)(unsafe.Pointer(&v4l2Buf.timecode)), - Sequence: uint32(v4l2Buf.sequence), - Memory: uint32(v4l2Buf.memory), - Info: *(*BufferInfo)(unsafe.Pointer(&v4l2Buf.m[0])), - Length: uint32(v4l2Buf.length), - Reserved2: uint32(v4l2Buf.reserved2), - RequestFD: *(*int32)(unsafe.Pointer(&v4l2Buf.anon0[0])), + Index: uint32(v4l2Buf.index), + Type: uint32(v4l2Buf._type), + BytesUsed: uint32(v4l2Buf.bytesused), + Flags: uint32(v4l2Buf.flags), + Field: uint32(v4l2Buf.field), + Timestamp: *(*sys.Timeval)(unsafe.Pointer(&v4l2Buf.timestamp)), + Timecode: *(*Timecode)(unsafe.Pointer(&v4l2Buf.timecode)), + Sequence: uint32(v4l2Buf.sequence), + Memory: uint32(v4l2Buf.memory), + Info: *(*BufferInfo)(unsafe.Pointer(&v4l2Buf.m[0])), + Length: uint32(v4l2Buf.length), + Reserved2: uint32(v4l2Buf.reserved2), + RequestFD: *(*int32)(unsafe.Pointer(&v4l2Buf.anon0[0])), } } @@ -144,21 +144,21 @@ func StreamOff(fd uintptr) error { return nil } -// InitBuffers sends buffer allocation request to initialize buffer IO -// for video capture when using either mem map, user pointer, or DMA buffers. +// RequestBuffersInfo sends buffer allocation request to initialize buffer IO +// for video capture or video output when using either mem map, user pointer, or DMA buffers. // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html#vidioc-reqbufs -func InitBuffers(fd uintptr, buffSize uint32) (RequestBuffers, error) { +func RequestBuffersInfo(fd uintptr, ioType IOType, bufType BufType, buffSize uint32) (RequestBuffers, error) { + if ioType != IOTypeMMAP && ioType != IOTypeDMABuf { + return RequestBuffers{}, fmt.Errorf("request buffers: %w", ErrorUnsupported) + } var req C.struct_v4l2_requestbuffers req.count = C.uint(buffSize) - req._type = C.uint(BufTypeVideoCapture) - req.memory = C.uint(StreamTypeMMAP) + req._type = C.uint(bufType) + req.memory = C.uint(ioType) if err := send(fd, C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { return RequestBuffers{}, fmt.Errorf("request buffers: %w", err) } - if req.count < 2 { - return RequestBuffers{}, errors.New("request buffers: insufficient memory on device") - } return *(*RequestBuffers)(unsafe.Pointer(&req)), nil } @@ -168,7 +168,7 @@ func InitBuffers(fd uintptr, buffSize uint32) (RequestBuffers, error) { func GetBuffer(fd uintptr, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf.memory = C.uint(IOTypeMMAP) v4l2Buf.index = C.uint(index) if err := send(fd, C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { @@ -202,7 +202,7 @@ func UnmapMemoryBuffer(buf []byte) error { func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf.memory = C.uint(IOTypeMMAP) v4l2Buf.index = C.uint(index) if err := send(fd, C.VIDIOC_QBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { @@ -219,7 +219,7 @@ func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { func DequeueBuffer(fd uintptr) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(StreamTypeMMAP) + v4l2Buf.memory = C.uint(IOTypeMMAP) if err := send(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("buffer dequeue: %w", err) From b2fbce4f0847c493db5ec9175f6c88c1c62826ab Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sun, 17 Apr 2022 17:51:06 -0400 Subject: [PATCH 03/11] Adding device configuration options --- examples/webcam/webcam.go | 32 ++++++---------- v4l2/device/device.go | 73 ++++++++++++++++++++++++------------ v4l2/device/device_config.go | 24 ++++++++++++ v4l2/stream_param.go | 8 ++-- v4l2/streaming.go | 14 +++---- 5 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 v4l2/device/device_config.go diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index 365f8fd..ff1fb28 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -54,7 +54,7 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) for frame := range frames { - if len(frame) == 0{ + if len(frame) == 0 { log.Print("skipping empty frame") continue } @@ -133,7 +133,11 @@ func main() { } // open device and setup device - device, err := device.Open(devName) + device, err := device.Open(devName, + device.WithIOType(v4l2.IOTypeMMAP), + device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}), + ) + if err != nil { log.Fatalf("failed to open device: %s", err) } @@ -148,15 +152,7 @@ func main() { log.Fatalf("unable to get format: %s", err) } log.Printf("Current format: %s", currFmt) - if err := device.SetPixFormat(updateFormat(currFmt, format, width, height)); err != nil { - log.Fatalf("failed to set format: %s", err) - } - currFmt, err = device.GetPixFormat() - if err != nil { - log.Fatalf("unable to get format: %s", err) - } pixfmt = currFmt.PixelFormat - log.Printf("Updated format: %s", currFmt) // Setup and start stream capture if err := device.StartStream(2); err != nil { @@ -188,18 +184,14 @@ func main() { } } -func updateFormat(pix v4l2.PixFormat, fmtStr string, w, h int) v4l2.PixFormat { - pix.Width = uint32(w) - pix.Height = uint32(h) - +func getFormatType(fmtStr string) v4l2.FourCCType { switch strings.ToLower(fmtStr) { case "mjpeg", "jpeg": - pix.PixelFormat = v4l2.PixelFmtMJPEG + return v4l2.PixelFmtMJPEG case "h264", "h.264": - pix.PixelFormat = v4l2.PixelFmtH264 + return v4l2.PixelFmtH264 case "yuyv": - pix.PixelFormat = v4l2.PixelFmtYUYV + return v4l2.PixelFmtYUYV } - - return pix -} \ No newline at end of file + return v4l2.PixelFmtMPEG +} diff --git a/v4l2/device/device.go b/v4l2/device/device.go index d450484..e009bfc 100644 --- a/v4l2/device/device.go +++ b/v4l2/device/device.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "reflect" sys "syscall" "time" @@ -14,11 +15,10 @@ type Device struct { path string file *os.File fd uintptr + config Config bufType v4l2.BufType cap v4l2.Capability cropCap v4l2.CropCapability - selectedFormat v4l2.PixFormat - supportedFormats []v4l2.PixFormat buffers [][]byte requestedBuf v4l2.RequestBuffers streaming bool @@ -26,13 +26,26 @@ type Device struct { // Open creates opens the underlying device at specified path // and returns a *Device or an error if unable to open device. -func Open(path string) (*Device, error) { +func Open(path string, options ...Option) (*Device, error) { file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) if err != nil { return nil, fmt.Errorf("device open: %w", err) } - dev := &Device{path: path, file: file, fd: file.Fd()} + dev := &Device{path: path, file: file, fd: file.Fd(), config: Config{}} + // apply options + if len(options) > 0 { + for _, o := range options { + o(&dev.config) + } + } + + // ensures IOType is set + if reflect.ValueOf(dev.config.ioType).IsZero() { + dev.config.ioType = v4l2.IOTypeMMAP + } + + // set capability cap, err := v4l2.GetCapability(file.Fd()) if err != nil { if err := file.Close(); err != nil { @@ -54,6 +67,7 @@ func Open(path string) (*Device, error) { return nil, fmt.Errorf("device open: %s: %w", path, v4l2.ErrorUnsupportedFeature) } + // set crop cropCap, err := v4l2.GetCropCapability(file.Fd(), dev.bufType) if err != nil { if err := file.Close(); err != nil { @@ -63,6 +77,15 @@ func Open(path string) (*Device, error) { } dev.cropCap = cropCap + // set pix format + if reflect.ValueOf(dev.config.pixFormat).IsZero() { + pixFmt, err := v4l2.GetPixFormat(file.Fd()) + if err != nil { + fmt.Errorf("device open: %s: set format: %w", path, err) + } + dev.config.pixFormat = pixFmt + } + return dev, nil } @@ -116,12 +139,16 @@ func (d *Device) GetPixFormat() (v4l2.PixFormat, error) { if !d.cap.IsVideoCaptureSupported() { return v4l2.PixFormat{}, v4l2.ErrorUnsupportedFeature } - pixFmt, err := v4l2.GetPixFormat(d.fd) - if err != nil { - return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) + + if reflect.ValueOf(d.config.pixFormat).IsZero() { + pixFmt, err := v4l2.GetPixFormat(d.fd) + if err != nil { + return v4l2.PixFormat{}, fmt.Errorf("device: %w", err) + } + d.config.pixFormat = pixFmt } - d.selectedFormat = pixFmt - return pixFmt, nil + + return d.config.pixFormat, nil } // SetPixFormat sets the pixel format for the associated device. @@ -133,7 +160,7 @@ func (d *Device) SetPixFormat(pixFmt v4l2.PixFormat) error { if err := v4l2.SetPixFormat(d.fd, pixFmt); err != nil { return fmt.Errorf("device: %w", err) } - d.selectedFormat = pixFmt + d.config.pixFormat = pixFmt return nil } @@ -175,24 +202,24 @@ func (d *Device) GetVideoInputInfo(index uint32) (v4l2.InputInfo, error) { // GetStreamParam returns streaming parameter information for device func (d *Device) GetStreamParam() (v4l2.StreamParam, error) { - if !d.cap.IsVideoCaptureSupported() { + if !d.cap.IsVideoCaptureSupported() && d.cap.IsVideoOutputSupported() { return v4l2.StreamParam{}, v4l2.ErrorUnsupportedFeature } - return v4l2.GetStreamParam(d.fd) + return v4l2.GetStreamParam(d.fd, d.bufType) } // SetStreamParam saves stream parameters for device -func (d *Device) SetStreamParam() (v4l2.StreamParam, error) { - if !d.cap.IsVideoCaptureSupported() { - return v4l2.StreamParam{}, v4l2.ErrorUnsupportedFeature +func (d *Device) SetStreamParam(param v4l2.StreamParam) error { + if !d.cap.IsVideoCaptureSupported() && d.cap.IsVideoOutputSupported() { + return v4l2.ErrorUnsupportedFeature } - return v4l2.GetStreamParam(d.fd) + return v4l2.SetStreamParam(d.fd, d.bufType, param) } -// SetCaptureFramerate sets the video capture FPS value of the device -func (d *Device) SetCaptureFramerate (fps uint32) error { - param := v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator:1,Denominator: fps}} - return v4l2.SetStreamParam(d.fd, v4l2.StreamParam{Capture: param}) +// SetCaptureFPS sets the video capture FPS value of the device +func (d *Device) SetCaptureFPS(fps uint32) error { + capture := v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} + return d.SetStreamParam(v4l2.StreamParam{Capture: capture}) } // GetMediaInfo returns info for a device that supports the Media API @@ -206,7 +233,7 @@ func (d *Device) StartStream(buffSize uint32) error { } // allocate device buffers - bufReq, err := v4l2.RequestBuffersInfo(d.fd, v4l2.IOTypeMMAP, d.bufType, buffSize) + bufReq, err := v4l2.InitBuffers(d.fd, d.config.ioType, d.bufType, buffSize) if err != nil { return fmt.Errorf("device: start stream: %w", err) } @@ -216,7 +243,7 @@ func (d *Device) StartStream(buffSize uint32) error { bufCount := int(d.requestedBuf.Count) d.buffers = make([][]byte, d.requestedBuf.Count) for i := 0; i < bufCount; i++ { - buffer, err := v4l2.GetBuffer(d.fd, uint32(i)) + buffer, err := v4l2.GetBuffer(d.fd, v4l2.IOTypeMMAP, d.bufType, uint32(i)) if err != nil { return fmt.Errorf("device start stream: %w", err) } @@ -320,4 +347,4 @@ func (d *Device) StopStream() error { return fmt.Errorf("device: stop stream: %w", err) } return nil -} \ No newline at end of file +} diff --git a/v4l2/device/device_config.go b/v4l2/device/device_config.go new file mode 100644 index 0000000..23c9fb0 --- /dev/null +++ b/v4l2/device/device_config.go @@ -0,0 +1,24 @@ +package device + +import ( + "github.com/vladimirvivien/go4vl/v4l2" +) + +type Config struct { + ioType v4l2.IOType + pixFormat v4l2.PixFormat +} + +type Option func(*Config) + +func WithIOType(ioType v4l2.IOType) Option { + return func(o *Config) { + o.ioType = ioType + } +} + +func WithPixFormat(pixFmt v4l2.PixFormat) Option { + return func(o *Config) { + o.pixFormat = pixFmt + } +} \ No newline at end of file diff --git a/v4l2/stream_param.go b/v4l2/stream_param.go index 1302e85..56b20c0 100644 --- a/v4l2/stream_param.go +++ b/v4l2/stream_param.go @@ -54,9 +54,9 @@ type OutputParam struct { // GetStreamParam returns streaming parameters for the driver (v4l2_streamparm). // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-g-parm.html // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L2362 -func GetStreamParam(fd uintptr) (StreamParam, error) { +func GetStreamParam(fd uintptr, bufType BufType) (StreamParam, error) { var v4l2Param C.struct_v4l2_streamparm - v4l2Param._type = C.uint(BufTypeVideoCapture) + v4l2Param._type = C.uint(bufType) if err := send(fd, C.VIDIOC_G_PARM, uintptr(unsafe.Pointer(&v4l2Param))); err != nil { return StreamParam{}, fmt.Errorf("stream param: %w", err) @@ -72,9 +72,9 @@ func GetStreamParam(fd uintptr) (StreamParam, error) { }, nil } -func SetStreamParam(fd uintptr, param StreamParam) error { +func SetStreamParam(fd uintptr, bufType BufType, param StreamParam) error { var v4l2Param C.struct_v4l2_streamparm - v4l2Param._type = C.uint(BufTypeVideoCapture) + v4l2Param._type = C.uint(bufType) *(*C.struct_v4l2_captureparm)(unsafe.Pointer(&v4l2Param.parm[0])) = *(*C.struct_v4l2_captureparm)(unsafe.Pointer(¶m.Capture)) *(*C.struct_v4l2_outputparm)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Param.parm[0])) + unsafe.Sizeof(C.struct_v4l2_captureparam{}))) = *(*C.struct_v4l2_outputparm)(unsafe.Pointer(¶m.Output)) diff --git a/v4l2/streaming.go b/v4l2/streaming.go index 5c0fb1b..df48929 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -144,10 +144,10 @@ func StreamOff(fd uintptr) error { return nil } -// RequestBuffersInfo sends buffer allocation request to initialize buffer IO +// InitBuffers sends buffer allocation request to initialize buffer IO // for video capture or video output when using either mem map, user pointer, or DMA buffers. // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html#vidioc-reqbufs -func RequestBuffersInfo(fd uintptr, ioType IOType, bufType BufType, buffSize uint32) (RequestBuffers, error) { +func InitBuffers(fd uintptr, ioType IOType, bufType BufType, buffSize uint32) (RequestBuffers, error) { if ioType != IOTypeMMAP && ioType != IOTypeDMABuf { return RequestBuffers{}, fmt.Errorf("request buffers: %w", ErrorUnsupported) } @@ -163,12 +163,12 @@ func RequestBuffersInfo(fd uintptr, ioType IOType, bufType BufType, buffSize uin return *(*RequestBuffers)(unsafe.Pointer(&req)), nil } -// GetBuffer retrieves bunffer info for allocated buffers at provided index. -// This call should take place after buffers are allocated (for mmap for instance). -func GetBuffer(fd uintptr, index uint32) (Buffer, error) { +// GetBuffer retrieves buffer info for allocated buffers at provided index. +// This call should take place after buffers are allocated with RequestBuffers (for mmap for instance). +func GetBuffer(fd uintptr, ioType IOType, bufType BufType, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(IOTypeMMAP) + v4l2Buf._type = C.uint(bufType) + v4l2Buf.memory = C.uint(ioType) v4l2Buf.index = C.uint(index) if err := send(fd, C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { From d1c84f0f999cd93edef83402c22606b6fc496f83 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Tue, 19 Apr 2022 23:27:23 -0400 Subject: [PATCH 04/11] Refactor streaming; separate stream loop from device; introduce new Device interface --- examples/capture/capture.go | 17 +-- examples/device_info/devinfo.go | 50 +++++-- examples/format/devfmt.go | 49 ++++--- examples/webcam/webcam.go | 8 +- v4l2/control.go | 2 + v4l2/device/device.go | 224 ++++++++++++++++---------------- v4l2/device/device_config.go | 14 ++ v4l2/stream_param.go | 16 ++- v4l2/streaming.go | 116 +++++++++++------ v4l2/streaming_loop.go | 76 +++++++++++ v4l2/types.go | 11 ++ 11 files changed, 378 insertions(+), 205 deletions(-) create mode 100644 v4l2/streaming_loop.go create mode 100644 v4l2/types.go diff --git a/examples/capture/capture.go b/examples/capture/capture.go index 710e606..4d572cd 100644 --- a/examples/capture/capture.go +++ b/examples/capture/capture.go @@ -23,6 +23,11 @@ func main() { } defer device.Close() + fps, err := device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get framerate: %s", err) + } + // helper function to search for format descriptions findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription { for _, desc := range fmts { @@ -90,21 +95,17 @@ func main() { log.Printf("Pixel format set to [%s]", pixFmt) // start stream - log.Println("Start capturing...") - if err := device.StartStream(3); err != nil { - log.Fatalf("failed to start stream: %s", err) - } - ctx, cancel := context.WithCancel(context.TODO()) - frameChan, err := device.Capture(ctx, 15) + frameChan, err := device.StartStream(ctx) if err != nil { - log.Fatal(err) + log.Fatalf("failed to stream: %s", err) } + // process frames from capture channel totalFrames := 10 count := 0 - log.Println("Streaming frames from device...") + log.Printf("Capturing %d frames at %d fps...", totalFrames, fps) for frame := range frameChan { fileName := fmt.Sprintf("capture_%d.jpg", count) file, err := os.Create(fileName) diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index f1acce3..644a31a 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -49,9 +49,18 @@ func main() { log.Fatal(err) } - if err := printCaptureParam(device); err != nil { - log.Fatal(err) + if device.Capability().IsVideoCaptureSupported() { + if err := printCaptureParam(device); err != nil { + log.Fatal(err) + } + } + + if device.Capability().IsVideoOutputSupported() { + if err := printOutputParam(device); err != nil { + log.Fatal(err) + } } + } func listDevices() error { @@ -236,23 +245,48 @@ func printCropInfo(dev *device.Device) error { func printCaptureParam(dev *device.Device) error { params, err := dev.GetStreamParam() if err != nil { - return fmt.Errorf("streaming capture param: %w", err) + return fmt.Errorf("stream capture param: %w", err) } - fmt.Println("Streaming parameters for video capture:") + fmt.Println("Stream capture parameters:") tpf := "not specified" - if params.Capability == v4l2.StreamParamTimePerFrame { + if params.Capture.Capability == v4l2.StreamParamTimePerFrame { tpf = "time per frame" } fmt.Printf(template, "Capability", tpf) hiqual := "not specified" - if params.CaptureMode == v4l2.StreamParamModeHighQuality { + if params.Capture.CaptureMode == v4l2.StreamParamModeHighQuality { hiqual = "high quality" } fmt.Printf(template, "Capture mode", hiqual) - fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.TimePerFrame.Denominator, params.TimePerFrame.Numerator)) - fmt.Printf(template, "Read buffers", fmt.Sprintf("%d", params.ReadBuffers)) + fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.Capture.TimePerFrame.Denominator, params.Capture.TimePerFrame.Numerator)) + fmt.Printf(template, "Read buffers", fmt.Sprintf("%d", params.Capture.ReadBuffers)) + return nil +} + + +func printOutputParam(dev *device.Device) error { + params, err := dev.GetStreamParam() + if err != nil { + return fmt.Errorf("stream output param: %w", err) + } + fmt.Println("Stream output parameters:") + + tpf := "not specified" + if params.Output.Capability == v4l2.StreamParamTimePerFrame { + tpf = "time per frame" + } + fmt.Printf(template, "Capability", tpf) + + hiqual := "not specified" + if params.Output.CaptureMode == v4l2.StreamParamModeHighQuality { + hiqual = "high quality" + } + fmt.Printf(template, "Output mode", hiqual) + + fmt.Printf(template, "Frames per second", fmt.Sprintf("%d/%d", params.Output.TimePerFrame.Denominator, params.Output.TimePerFrame.Numerator)) + fmt.Printf(template, "Write buffers", fmt.Sprintf("%d", params.Output.WriteBuffers)) return nil } diff --git a/examples/format/devfmt.go b/examples/format/devfmt.go index 47d41c9..bcc1427 100644 --- a/examples/format/devfmt.go +++ b/examples/format/devfmt.go @@ -21,18 +21,6 @@ func main() { flag.StringVar(&format, "f", format, "pixel format") flag.Parse() - device, err := device.Open(devName) - if err != nil { - log.Fatalf("failed to open device: %s", err) - } - defer device.Close() - - currFmt, err := device.GetPixFormat() - if err != nil { - log.Fatalf("unable to get format: %s", err) - } - log.Printf("Current format: %s", currFmt) - fmtEnc := v4l2.PixelFmtYUYV switch strings.ToLower(format) { case "mjpeg": @@ -43,18 +31,37 @@ func main() { fmtEnc = v4l2.PixelFmtYUYV } - if err := device.SetPixFormat(v4l2.PixFormat{ - Width: uint32(width), - Height: uint32(height), - PixelFormat: fmtEnc, - Field: v4l2.FieldNone, - }); err != nil { - log.Fatalf("failed to set format: %s", err) + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{Width: uint32(width), Height: uint32(height), PixelFormat: fmtEnc, Field: v4l2.FieldNone}), + device.WithFPS(15), + ) + if err != nil { + log.Fatalf("failed to open device: %s", err) } + defer device.Close() - currFmt, err = device.GetPixFormat() + currFmt, err := device.GetPixFormat() if err != nil { log.Fatalf("unable to get format: %s", err) } - log.Printf("Updated format: %s", currFmt) + log.Printf("Current format: %s", currFmt) + + // FPS + fps, err := device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get fps: %s", err) + } + log.Printf("current frame rate: %d fps", fps) + // update fps + if fps < 30 { + if err := device.SetFrameRate(30); err != nil{ + log.Fatalf("failed to set frame rate: %s", err) + } + } + fps, err = device.GetFrameRate() + if err != nil { + log.Fatalf("failed to get fps: %s", err) + } + log.Printf("updated frame rate: %d fps", fps) } \ No newline at end of file diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index ff1fb28..c551e40 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -136,6 +136,7 @@ func main() { device, err := device.Open(devName, device.WithIOType(v4l2.IOTypeMMAP), device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}), + device.WithFPS(uint32(frameRate)), ) if err != nil { @@ -154,14 +155,9 @@ func main() { log.Printf("Current format: %s", currFmt) pixfmt = currFmt.PixelFormat - // Setup and start stream capture - if err := device.StartStream(2); err != nil { - log.Fatalf("unable to start stream: %s", err) - } - // start capture ctx, cancel := context.WithCancel(context.TODO()) - f, err := device.Capture(ctx, uint32(frameRate)) + f, err := device.StartStream(ctx) if err != nil { log.Fatalf("stream capture: %s", err) } diff --git a/v4l2/control.go b/v4l2/control.go index 0cb4074..9ba5b2e 100644 --- a/v4l2/control.go +++ b/v4l2/control.go @@ -15,6 +15,7 @@ type Control struct { Value uint32 } +// GetControl returns control value for specified ID func GetControl(fd uintptr, id uint32) (Control, error) { var ctrl C.struct_v4l2_control ctrl.id = C.uint(id) @@ -29,6 +30,7 @@ func GetControl(fd uintptr, id uint32) (Control, error) { }, nil } +// SetControl applies control value for specified ID func SetControl(fd uintptr, id, value uint32) error { var ctrl C.struct_v4l2_control ctrl.id = C.uint(id) diff --git a/v4l2/device/device.go b/v4l2/device/device.go index e009bfc..182c0d4 100644 --- a/v4l2/device/device.go +++ b/v4l2/device/device.go @@ -6,28 +6,28 @@ import ( "os" "reflect" sys "syscall" - "time" "github.com/vladimirvivien/go4vl/v4l2" ) type Device struct { - path string - file *os.File - fd uintptr - config Config - bufType v4l2.BufType - cap v4l2.Capability - cropCap v4l2.CropCapability - buffers [][]byte - requestedBuf v4l2.RequestBuffers - streaming bool + path string + file *os.File + fd uintptr + config Config + bufType v4l2.BufType + cap v4l2.Capability + cropCap v4l2.CropCapability + buffers [][]byte + requestedBuf v4l2.RequestBuffers + streaming bool } // Open creates opens the underlying device at specified path // and returns a *Device or an error if unable to open device. func Open(path string, options ...Option) (*Device, error) { file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) + //file, err := os.OpenFile(path, sys.O_RDWR, 0644) if err != nil { return nil, fmt.Errorf("device open: %w", err) } @@ -78,12 +78,22 @@ func Open(path string, options ...Option) (*Device, error) { dev.cropCap = cropCap // set pix format - if reflect.ValueOf(dev.config.pixFormat).IsZero() { - pixFmt, err := v4l2.GetPixFormat(file.Fd()) - if err != nil { + if !reflect.ValueOf(dev.config.pixFormat).IsZero() { + if err := dev.SetPixFormat(dev.config.pixFormat); err != nil { fmt.Errorf("device open: %s: set format: %w", path, err) } - dev.config.pixFormat = pixFmt + } + + // set fps + if !reflect.ValueOf(dev.config.fps).IsZero() { + if err := dev.SetFrameRate(dev.config.fps); err != nil { + fmt.Errorf("device open: %s: set fps: %w", path, err) + } + } + + // set preferred device buffer size + if reflect.ValueOf(dev.config.bufSize).IsZero() { + dev.config.bufSize = 2 } return dev, nil @@ -101,8 +111,8 @@ func (d *Device) Close() error { } // Name returns the device name (or path) -func (d *Device) Name() uintptr { - return d.fd +func (d *Device) Name() string { + return d.path } // FileDescriptor returns the file descriptor value for the device @@ -110,11 +120,34 @@ func (d *Device) FileDescriptor() uintptr { return d.fd } +// Buffers returns the internal mapped buffers. This method should be +// called after streaming has been started otherwise it may return nil. +func (d *Device) Buffers() [][]byte { + return d.buffers +} + // Capability returns device capability info. func (d *Device) Capability() v4l2.Capability { return d.cap } +// BufferType this is a convenience method that returns the device mode (i.e. Capture, Output, etc) +// Use method Capability for detail about the device. +func (d *Device) BufferType() v4l2.BufType { + return d.bufType +} + +// BufferCount returns configured number of buffers to be used during streaming. +// If called after streaming start, this value could be updated by the driver. +func (d *Device) BufferCount() v4l2.BufType { + return d.config.bufSize +} + +// MemIOType returns the device memory input/output type (i.e. Memory mapped, DMA, user pointer, etc) +func (d *Device) MemIOType() v4l2.IOType { + return d.config.ioType +} + // GetCropCapability returns cropping info for device func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { if !d.cap.IsVideoCaptureSupported() { @@ -216,10 +249,42 @@ func (d *Device) SetStreamParam(param v4l2.StreamParam) error { return v4l2.SetStreamParam(d.fd, d.bufType, param) } -// SetCaptureFPS sets the video capture FPS value of the device -func (d *Device) SetCaptureFPS(fps uint32) error { - capture := v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} - return d.SetStreamParam(v4l2.StreamParam{Capture: capture}) +// SetFrameRate sets the FPS rate value of the device +func (d *Device) SetFrameRate(fps uint32) error { + var param v4l2.StreamParam + switch { + case d.cap.IsVideoCaptureSupported(): + param.Capture = v4l2.CaptureParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} + case d.cap.IsVideoOutputSupported(): + param.Output = v4l2.OutputParam{TimePerFrame: v4l2.Fract{Numerator: 1, Denominator: fps}} + default: + return v4l2.ErrorUnsupportedFeature + } + if err := d.SetStreamParam(param); err != nil { + return fmt.Errorf("device: set fps: %w", err) + } + d.config.fps = fps + return nil +} + +// GetFrameRate returns the FPS value for the device +func (d *Device) GetFrameRate() (uint32, error) { + if reflect.ValueOf(d.config.fps).IsZero() { + param, err := d.GetStreamParam() + if err != nil { + return 0, fmt.Errorf("device: frame rate: %w", err) + } + switch { + case d.cap.IsVideoCaptureSupported(): + d.config.fps = param.Capture.TimePerFrame.Denominator + case d.cap.IsVideoOutputSupported(): + d.config.fps = param.Output.TimePerFrame.Denominator + default: + return 0, v4l2.ErrorUnsupportedFeature + } + } + + return d.config.fps, nil } // GetMediaInfo returns info for a device that supports the Media API @@ -227,123 +292,56 @@ func (d *Device) GetMediaInfo() (v4l2.MediaDeviceInfo, error) { return v4l2.GetMediaDeviceInfo(d.fd) } -func (d *Device) StartStream(buffSize uint32) error { +func (d *Device) StartStream(ctx context.Context) (<-chan []byte, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if !d.cap.IsStreamingSupported() { + return nil, fmt.Errorf("device: start stream: %s", v4l2.ErrorUnsupportedFeature) + } + if d.streaming { - return nil + return nil, fmt.Errorf("device: stream already started") } // allocate device buffers - bufReq, err := v4l2.InitBuffers(d.fd, d.config.ioType, d.bufType, buffSize) + bufReq, err := v4l2.InitBuffers(d) if err != nil { - return fmt.Errorf("device: start stream: %w", err) + return nil, fmt.Errorf("device: init buffers: %w", err) } + d.config.bufSize = bufReq.Count // update with granted buf size d.requestedBuf = bufReq - // for each device buff allocated, prepare local mapped buffer - bufCount := int(d.requestedBuf.Count) - d.buffers = make([][]byte, d.requestedBuf.Count) - for i := 0; i < bufCount; i++ { - buffer, err := v4l2.GetBuffer(d.fd, v4l2.IOTypeMMAP, d.bufType, uint32(i)) - if err != nil { - return fmt.Errorf("device start stream: %w", err) - } - - offset := buffer.Info.Offset - length := buffer.Length - mappedBuf, err := v4l2.MapMemoryBuffer(d.fd, int64(offset), int(length)) - if err != nil { - return fmt.Errorf("device start stream: %w", err) - } - d.buffers[i] = mappedBuf + // for each allocated device buf, map into local space + if d.buffers, err = v4l2.MakeMappedBuffers(d); err != nil { + return nil, fmt.Errorf("device: make mapped buffers: %s", err) } // Initial enqueue of buffers for capture - for i := 0; i < bufCount; i++ { - _, err := v4l2.QueueBuffer(d.fd, uint32(i)) + for i := 0; i < int(d.config.bufSize); i++ { + _, err := v4l2.QueueBuffer(d.fd, d.config.ioType, d.bufType, uint32(i)) if err != nil { - return fmt.Errorf("device start stream: %w", err) + return nil, fmt.Errorf("device: initial buffer queueing: %w", err) } } - // turn on device stream - if err := v4l2.StreamOn(d.fd); err != nil { - return fmt.Errorf("device start stream: %w", err) + dataChan, err := v4l2.StartStreamLoop(ctx, d) + if err != nil { + return nil, fmt.Errorf("device: start stream loop: %s", err) } d.streaming = true - return nil -} - -// Capture captures video buffer from device and emit -// each buffer on channel. -func (d *Device) Capture(ctx context.Context, fps uint32) (<-chan []byte, error) { - if !d.streaming { - return nil, fmt.Errorf("device: capture: streaming not started") - } - if ctx == nil { - return nil, fmt.Errorf("device: context nil") - } - - bufCount := int(d.requestedBuf.Count) - dataChan := make(chan []byte, bufCount) - - if fps == 0 { - fps = 10 - } - - // delay duration based on frame per second - fpsDelay := time.Duration((float64(1) / float64(fps)) * float64(time.Second)) - - go func() { - defer close(dataChan) - - // capture forever or until signaled to stop - for { - // capture bufCount frames - for i := 0; i < bufCount; i++ { - //TODO add better error-handling during capture, for now just panic - if err := v4l2.WaitForDeviceRead(d.fd, 2*time.Second); err != nil { - panic(fmt.Errorf("device: capture: %w", err).Error()) - } - - // dequeue the device buf - bufInfo, err := v4l2.DequeueBuffer(d.fd) - if err != nil { - panic(fmt.Errorf("device: capture: %w", err).Error()) - } - - // assert dequeued buffer is in proper range - if !(int(bufInfo.Index) < bufCount) { - panic(fmt.Errorf("device: capture: unexpected device buffer index: %d", bufInfo.Index).Error()) - } - - select { - case dataChan <- d.buffers[bufInfo.Index][:bufInfo.BytesUsed]: - case <-ctx.Done(): - return - } - // enqueu used buffer, prepare for next read - if _, err := v4l2.QueueBuffer(d.fd, bufInfo.Index); err != nil { - panic(fmt.Errorf("device capture: %w", err).Error()) - } - - time.Sleep(fpsDelay) - } - } - }() - return dataChan, nil } func (d *Device) StopStream() error { d.streaming = false - for i := 0; i < len(d.buffers); i++ { - if err := v4l2.UnmapMemoryBuffer(d.buffers[i]); err != nil { - return fmt.Errorf("device: stop stream: %w", err) - } + if err := v4l2.UnmapBuffers(d); err != nil { + return fmt.Errorf("device: stop stream: %s", err) } - if err := v4l2.StreamOff(d.fd); err != nil { + if err := v4l2.StopStreamLoop(d); err != nil { return fmt.Errorf("device: stop stream: %w", err) } return nil diff --git a/v4l2/device/device_config.go b/v4l2/device/device_config.go index 23c9fb0..e9aabdc 100644 --- a/v4l2/device/device_config.go +++ b/v4l2/device/device_config.go @@ -7,6 +7,8 @@ import ( type Config struct { ioType v4l2.IOType pixFormat v4l2.PixFormat + bufSize uint32 + fps uint32 } type Option func(*Config) @@ -21,4 +23,16 @@ func WithPixFormat(pixFmt v4l2.PixFormat) Option { return func(o *Config) { o.pixFormat = pixFmt } +} + +func WithBufferSize(size uint32) Option { + return func(o *Config) { + o.bufSize = size + } +} + +func WithFPS(fps uint32) Option { + return func(o *Config) { + o.fps = fps + } } \ No newline at end of file diff --git a/v4l2/stream_param.go b/v4l2/stream_param.go index 56b20c0..040cf79 100644 --- a/v4l2/stream_param.go +++ b/v4l2/stream_param.go @@ -73,13 +73,17 @@ func GetStreamParam(fd uintptr, bufType BufType) (StreamParam, error) { } func SetStreamParam(fd uintptr, bufType BufType, param StreamParam) error { - var v4l2Param C.struct_v4l2_streamparm - v4l2Param._type = C.uint(bufType) - *(*C.struct_v4l2_captureparm)(unsafe.Pointer(&v4l2Param.parm[0])) = *(*C.struct_v4l2_captureparm)(unsafe.Pointer(¶m.Capture)) - *(*C.struct_v4l2_outputparm)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Param.parm[0])) + unsafe.Sizeof(C.struct_v4l2_captureparam{}))) = - *(*C.struct_v4l2_outputparm)(unsafe.Pointer(¶m.Output)) + var v4l2Parm C.struct_v4l2_streamparm + v4l2Parm._type = C.uint(bufType) + if bufType == BufTypeVideoCapture { + *(*C.struct_v4l2_captureparm)(unsafe.Pointer(&v4l2Parm.parm[0])) = *(*C.struct_v4l2_captureparm)(unsafe.Pointer(¶m.Capture)) + } + if bufType == BufTypeVideoOutput { + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(uintptr(unsafe.Pointer(&v4l2Parm.parm[0])) + unsafe.Sizeof(v4l2Parm.parm[0]))) = + *(*C.struct_v4l2_outputparm)(unsafe.Pointer(¶m.Output)) + } - if err := send(fd, C.VIDIOC_S_PARM, uintptr(unsafe.Pointer(&v4l2Param))); err != nil { + if err := send(fd, C.VIDIOC_S_PARM, uintptr(unsafe.Pointer(&v4l2Parm))); err != nil { return fmt.Errorf("stream param: %w", err) } diff --git a/v4l2/streaming.go b/v4l2/streaming.go index df48929..e2b5235 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -4,9 +4,7 @@ package v4l2 import "C" import ( - "errors" "fmt" - "time" "unsafe" sys "golang.org/x/sys/unix" @@ -125,9 +123,9 @@ type PlaneInfo struct { // StreamOn requests streaming to be turned on for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOn(fd uintptr) error { - bufType := BufTypeVideoCapture - if err := send(fd, C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { +func StreamOn(dev Device) error { + bufType := dev.BufferType() + if err := send(dev.FileDescriptor(), C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream on: %w", err) } return nil @@ -136,9 +134,9 @@ func StreamOn(fd uintptr) error { // StreamOff requests streaming to be turned off for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOff(fd uintptr) error { - bufType := BufTypeVideoCapture - if err := send(fd, C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { +func StreamOff(dev Device) error { + bufType := dev.BufferType() + if err := send(dev.FileDescriptor(), C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream off: %w", err) } return nil @@ -147,16 +145,16 @@ func StreamOff(fd uintptr) error { // InitBuffers sends buffer allocation request to initialize buffer IO // for video capture or video output when using either mem map, user pointer, or DMA buffers. // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html#vidioc-reqbufs -func InitBuffers(fd uintptr, ioType IOType, bufType BufType, buffSize uint32) (RequestBuffers, error) { - if ioType != IOTypeMMAP && ioType != IOTypeDMABuf { +func InitBuffers(dev Device) (RequestBuffers, error) { + if dev.MemIOType() != IOTypeMMAP && dev.MemIOType() != IOTypeDMABuf { return RequestBuffers{}, fmt.Errorf("request buffers: %w", ErrorUnsupported) } var req C.struct_v4l2_requestbuffers - req.count = C.uint(buffSize) - req._type = C.uint(bufType) - req.memory = C.uint(ioType) + req.count = C.uint(dev.BufferCount()) + req._type = C.uint(dev.BufferType()) + req.memory = C.uint(dev.MemIOType()) - if err := send(fd, C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { + if err := send(dev.FileDescriptor(), C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { return RequestBuffers{}, fmt.Errorf("request buffers: %w", err) } @@ -165,13 +163,13 @@ func InitBuffers(fd uintptr, ioType IOType, bufType BufType, buffSize uint32) (R // GetBuffer retrieves buffer info for allocated buffers at provided index. // This call should take place after buffers are allocated with RequestBuffers (for mmap for instance). -func GetBuffer(fd uintptr, ioType IOType, bufType BufType, index uint32) (Buffer, error) { +func GetBuffer(dev Device, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(bufType) - v4l2Buf.memory = C.uint(ioType) + v4l2Buf._type = C.uint(dev.BufferType()) + v4l2Buf.memory = C.uint(dev.MemIOType()) v4l2Buf.index = C.uint(index) - if err := send(fd, C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { + if err := send(dev.FileDescriptor(), C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("query buffer: %w", err) } @@ -187,6 +185,27 @@ func MapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { return data, nil } +// MakeMappedBuffers creates mapped memory buffers for specified buffer count of device. +func MakeMappedBuffers(dev Device)([][]byte, error) { + bufCount := int(dev.BufferCount()) + buffers := make([][]byte, bufCount) + for i := 0; i < bufCount; i++ { + buffer, err := GetBuffer(dev, uint32(i)) + if err != nil { + return nil, fmt.Errorf("mapped buffers: %w", err) + } + + offset := buffer.Info.Offset + length := buffer.Length + mappedBuf, err := MapMemoryBuffer(dev.FileDescriptor(), int64(offset), int(length)) + if err != nil { + return nil, fmt.Errorf("mapped buffers: %w", err) + } + buffers[i] = mappedBuf + } + return buffers, nil +} + // UnmapMemoryBuffer removes the buffer that was previously mapped. func UnmapMemoryBuffer(buf []byte) error { if err := sys.Munmap(buf); err != nil { @@ -195,14 +214,27 @@ func UnmapMemoryBuffer(buf []byte) error { return nil } +// UnmapBuffers unmaps all mapped memory buffer for device +func UnmapBuffers(dev Device) error { + if dev.Buffers() == nil { + return fmt.Errorf("unmap buffers: uninitialized buffers") + } + for i := 0; i < len(dev.Buffers()); i++ { + if err := UnmapMemoryBuffer(dev.Buffers()[i]); err != nil { + return fmt.Errorf("unmap buffers: %w", err) + } + } + return nil +} + // QueueBuffer enqueues a buffer in the device driver (as empty for capturing, or filled for video output) // when using either memory map, user pointer, or DMA buffers. Buffer is returned with // additional information about the queued buffer. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf -func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { +func QueueBuffer(fd uintptr, ioType IOType, bufType BufType, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(IOTypeMMAP) + v4l2Buf._type = C.uint(bufType) + v4l2Buf.memory = C.uint(ioType) v4l2Buf.index = C.uint(index) if err := send(fd, C.VIDIOC_QBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { @@ -216,10 +248,10 @@ func QueueBuffer(fd uintptr, index uint32) (Buffer, error) { // when using either memory map, user pointer, or DMA buffers. Buffer is returned with // additional information about the dequeued buffer. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html#vidioc-qbuf -func DequeueBuffer(fd uintptr) (Buffer, error) { +func DequeueBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer - v4l2Buf._type = C.uint(BufTypeVideoCapture) - v4l2Buf.memory = C.uint(IOTypeMMAP) + v4l2Buf._type = C.uint(bufType) + v4l2Buf.memory = C.uint(ioType) if err := send(fd, C.VIDIOC_DQBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("buffer dequeue: %w", err) @@ -229,24 +261,22 @@ func DequeueBuffer(fd uintptr) (Buffer, error) { return makeBuffer(v4l2Buf), nil } -// WaitForDeviceRead blocks until the specified device is -// ready to be read or has timedout. -func WaitForDeviceRead(fd uintptr, timeout time.Duration) error { - timeval := sys.NsecToTimeval(timeout.Nanoseconds()) - var fdsRead sys.FdSet - fdsRead.Set(int(fd)) - for { - n, err := sys.Select(int(fd+1), &fdsRead, nil, nil, &timeval) - switch n { - case -1: - if err == sys.EINTR { - continue - } - return err - case 0: - return errors.New("wait for device ready: timeout") - default: - return nil - } +// CaptureFrame captures a frame buffer from the device +func CaptureFrame(dev Device) ([]byte, error) { + bufInfo, err := DequeueBuffer(dev.FileDescriptor(), dev.MemIOType(), dev.BufferType()) + if err != nil { + return nil, fmt.Errorf("capture frame: dequeue: %w", err) + } + // assert dequeued buffer is in proper range + if !(bufInfo.Index < dev.BufferCount()) { + return nil, fmt.Errorf("capture frame: buffer with unexpected index: %d (out of %d)", bufInfo.Index, dev.BufferCount()) } + + // requeue/clear used buffer, prepare for next read + if _, err := QueueBuffer(dev.FileDescriptor(), dev.MemIOType(), dev.BufferType(), bufInfo.Index); err != nil { + return nil, fmt.Errorf("capture frame: queue: %w", err) + } + + // return captured buffer + return dev.Buffers()[bufInfo.Index][:bufInfo.BytesUsed], nil } diff --git a/v4l2/streaming_loop.go b/v4l2/streaming_loop.go new file mode 100644 index 0000000..67171a9 --- /dev/null +++ b/v4l2/streaming_loop.go @@ -0,0 +1,76 @@ +package v4l2 + +import ( + "context" + "fmt" + + sys "golang.org/x/sys/unix" +) + +// StartStreamLoop issue a streaming request for the device and sets up +// a loop to capture incoming buffers from the device. +func StartStreamLoop(ctx context.Context, dev Device) (chan []byte, error) { + if err := StreamOn(dev); err != nil { + return nil, fmt.Errorf("stream loop: driver stream on: %w", err) + } + + dataChan := make(chan []byte, dev.BufferCount()) + + go func() { + defer close(dataChan) + for { + select { + case <-WaitForRead(dev): + //TODO add better error-handling, for now just panic + frame, err := CaptureFrame(dev) + if err != nil { + panic(fmt.Errorf("stream loop: frame capture: %s", err).Error()) + } + select { + case dataChan <-frame: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } + }() + + return dataChan, nil +} + +// StopStreamLoop unmaps allocated IO memory and signal device to stop streaming +func StopStreamLoop(dev Device) error { + if dev.Buffers() == nil { + return fmt.Errorf("stop loop: failed to stop loop: buffers uninitialized") + } + + if err := StreamOff(dev); err != nil { + return fmt.Errorf("stop loop: stream off: %w", err) + } + return nil +} + +func WaitForRead(dev Device) <-chan struct{} { + sigChan := make(chan struct{}) + + fd := dev.FileDescriptor() + + go func() { + defer close(sigChan) + var fdsRead sys.FdSet + fdsRead.Set(int(fd)) + for { + n, err := sys.Select(int(fd+1), &fdsRead, nil, nil, nil) + if n == -1 { + if err == sys.EINTR { + continue + } + } + sigChan <- struct{}{} + } + }() + + return sigChan +} diff --git a/v4l2/types.go b/v4l2/types.go new file mode 100644 index 0000000..39cc4d2 --- /dev/null +++ b/v4l2/types.go @@ -0,0 +1,11 @@ +package v4l2 + +type Device interface { + Name() string + FileDescriptor() uintptr + Capability() Capability + Buffers() [][]byte + BufferType() BufType + BufferCount() uint32 + MemIOType() IOType +} From 2b44a916cd886bd713cc6237aa2d465d3f859f22 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Fri, 22 Apr 2022 18:41:56 -0400 Subject: [PATCH 05/11] Refactor the device model for streaming The rewrite is working toward having a simpler model that can represent streaming capture/output devices. The device package determines how the device works when one is created and configured. --- {v4l2/device => device}/device.go | 157 ++++++++++++++++------- {v4l2/device => device}/device_config.go | 25 +++- device/doc.go | 2 + {v4l2/device => device}/list.go | 0 {v4l2/device => device}/list_test.go | 0 examples/capture/capture.go | 11 +- examples/device_info/devinfo.go | 30 ++--- examples/format/devfmt.go | 8 +- examples/webcam/webcam.go | 23 +++- imgsupport/converters.go | 4 + v4l2/streaming.go | 56 ++++---- v4l2/streaming_loop.go | 38 +----- v4l2/types.go | 32 ++++- 13 files changed, 237 insertions(+), 149 deletions(-) rename {v4l2/device => device}/device.go (72%) rename {v4l2/device => device}/device_config.go (51%) create mode 100644 device/doc.go rename {v4l2/device => device}/list.go (100%) rename {v4l2/device => device}/list_test.go (100%) diff --git a/v4l2/device/device.go b/device/device.go similarity index 72% rename from v4l2/device/device.go rename to device/device.go index 182c0d4..c614956 100644 --- a/v4l2/device/device.go +++ b/device/device.go @@ -14,25 +14,20 @@ type Device struct { path string file *os.File fd uintptr - config Config + config config bufType v4l2.BufType cap v4l2.Capability cropCap v4l2.CropCapability buffers [][]byte requestedBuf v4l2.RequestBuffers streaming bool + output chan []byte } -// Open creates opens the underlying device at specified path -// and returns a *Device or an error if unable to open device. +// Open creates opens the underlying device at specified path for streaming. +// It returns a *Device or an error if unable to open device. func Open(path string, options ...Option) (*Device, error) { - file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) - //file, err := os.OpenFile(path, sys.O_RDWR, 0644) - if err != nil { - return nil, fmt.Errorf("device open: %w", err) - } - dev := &Device{path: path, file: file, fd: file.Fd(), config: Config{}} - + dev := &Device{path: path, config: config{}} // apply options if len(options) > 0 { for _, o := range options { @@ -40,12 +35,15 @@ func Open(path string, options ...Option) (*Device, error) { } } - // ensures IOType is set - if reflect.ValueOf(dev.config.ioType).IsZero() { - dev.config.ioType = v4l2.IOTypeMMAP + file, err := os.OpenFile(path, sys.O_RDWR|sys.O_NONBLOCK, 0644) + //file, err := os.OpenFile(path, sys.O_RDWR, 0644) + if err != nil { + return nil, fmt.Errorf("device open: %w", err) } + dev.file = file + dev.fd = file.Fd() - // set capability + // get capability cap, err := v4l2.GetCapability(file.Fd()) if err != nil { if err := file.Close(); err != nil { @@ -55,9 +53,21 @@ func Open(path string, options ...Option) (*Device, error) { } dev.cap = cap + // set preferred device buffer size + if reflect.ValueOf(dev.config.bufSize).IsZero() { + dev.config.bufSize = 2 + } + + // only supports streaming IO model right now + if !dev.cap.IsStreamingSupported() { + return nil, fmt.Errorf("device open: only streaming IO is supported") + } + switch { case cap.IsVideoCaptureSupported(): + // setup capture parameters and chan for captured data dev.bufType = v4l2.BufTypeVideoCapture + dev.output = make(chan []byte, dev.config.bufSize) case cap.IsVideoOutputSupported(): dev.bufType = v4l2.BufTypeVideoOutput default: @@ -67,21 +77,24 @@ func Open(path string, options ...Option) (*Device, error) { return nil, fmt.Errorf("device open: %s: %w", path, v4l2.ErrorUnsupportedFeature) } - // set crop - cropCap, err := v4l2.GetCropCapability(file.Fd(), dev.bufType) - if err != nil { - if err := file.Close(); err != nil { - return nil, fmt.Errorf("device open: %s: closing after failure: %s", path, err) - } - return nil, fmt.Errorf("device open: %s: %w", path, err) + if !reflect.ValueOf(dev.config.bufType).IsZero() && dev.config.bufType != dev.bufType { + return nil, fmt.Errorf("device open: does not support buffer stream type") + } + + // ensures IOType is set + if reflect.ValueOf(dev.config.ioType).IsZero() { + dev.config.ioType = v4l2.IOTypeMMAP } - dev.cropCap = cropCap // set pix format if !reflect.ValueOf(dev.config.pixFormat).IsZero() { if err := dev.SetPixFormat(dev.config.pixFormat); err != nil { fmt.Errorf("device open: %s: set format: %w", path, err) } + } else { + if dev.config.pixFormat, err = v4l2.GetPixFormat(dev.fd); err != nil { + fmt.Errorf("device open: %s: get pix format: %w", path, err) + } } // set fps @@ -89,12 +102,12 @@ func Open(path string, options ...Option) (*Device, error) { if err := dev.SetFrameRate(dev.config.fps); err != nil { fmt.Errorf("device open: %s: set fps: %w", path, err) } + } else { + if dev.config.fps, err = dev.GetFrameRate(); err != nil { + fmt.Errorf("device open: %s: get fps: %w", path, err) + } } - // set preferred device buffer size - if reflect.ValueOf(dev.config.bufSize).IsZero() { - dev.config.bufSize = 2 - } return dev, nil } @@ -102,7 +115,7 @@ func Open(path string, options ...Option) (*Device, error) { // Close closes the underlying device associated with `d` . func (d *Device) Close() error { if d.streaming { - if err := d.StopStream(); err != nil { + if err := d.Stop(); err != nil { return err } } @@ -115,8 +128,8 @@ func (d *Device) Name() string { return d.path } -// FileDescriptor returns the file descriptor value for the device -func (d *Device) FileDescriptor() uintptr { +// Fd returns the file descriptor value for the device +func (d *Device) Fd() uintptr { return d.fd } @@ -148,6 +161,18 @@ func (d *Device) MemIOType() v4l2.IOType { return d.config.ioType } +// GetOutput returns the channel that outputs streamed data that is +// captured from the underlying device driver. +func (d *Device) GetOutput() <-chan []byte { + return d.output +} + +// SetInput sets up an input channel for data this sent for output to the +// underlying device driver. +func (d *Device) SetInput(in <-chan []byte) { + +} + // GetCropCapability returns cropping info for device func (d *Device) GetCropCapability() (v4l2.CropCapability, error) { if !d.cap.IsVideoCaptureSupported() { @@ -251,6 +276,10 @@ func (d *Device) SetStreamParam(param v4l2.StreamParam) error { // SetFrameRate sets the FPS rate value of the device func (d *Device) SetFrameRate(fps uint32) error { + if !d.cap.IsStreamingSupported() { + return fmt.Errorf("set frame rate: %w", v4l2.ErrorUnsupportedFeature) + } + var param v4l2.StreamParam switch { case d.cap.IsVideoCaptureSupported(): @@ -292,57 +321,93 @@ func (d *Device) GetMediaInfo() (v4l2.MediaDeviceInfo, error) { return v4l2.GetMediaDeviceInfo(d.fd) } -func (d *Device) StartStream(ctx context.Context) (<-chan []byte, error) { +func (d *Device) Start(ctx context.Context) error { if ctx.Err() != nil { - return nil, ctx.Err() + return ctx.Err() } if !d.cap.IsStreamingSupported() { - return nil, fmt.Errorf("device: start stream: %s", v4l2.ErrorUnsupportedFeature) + return fmt.Errorf("device: start stream: %s", v4l2.ErrorUnsupportedFeature) } if d.streaming { - return nil, fmt.Errorf("device: stream already started") + return fmt.Errorf("device: stream already started") } // allocate device buffers bufReq, err := v4l2.InitBuffers(d) if err != nil { - return nil, fmt.Errorf("device: init buffers: %w", err) + return fmt.Errorf("device: init buffers: %w", err) } d.config.bufSize = bufReq.Count // update with granted buf size d.requestedBuf = bufReq // for each allocated device buf, map into local space - if d.buffers, err = v4l2.MakeMappedBuffers(d); err != nil { - return nil, fmt.Errorf("device: make mapped buffers: %s", err) + if d.buffers, err = v4l2.MapMemoryBuffers(d); err != nil { + return fmt.Errorf("device: make mapped buffers: %s", err) } // Initial enqueue of buffers for capture for i := 0; i < int(d.config.bufSize); i++ { _, err := v4l2.QueueBuffer(d.fd, d.config.ioType, d.bufType, uint32(i)) if err != nil { - return nil, fmt.Errorf("device: initial buffer queueing: %w", err) + return fmt.Errorf("device: initial buffer queueing: %w", err) } } - dataChan, err := v4l2.StartStreamLoop(ctx, d) - if err != nil { - return nil, fmt.Errorf("device: start stream loop: %s", err) + if err := d.startStreamLoop(ctx); err != nil { + return fmt.Errorf("device: start stream loop: %s", err) } d.streaming = true - return dataChan, nil + return nil } -func (d *Device) StopStream() error { +func (d *Device) Stop() error { d.streaming = false - if err := v4l2.UnmapBuffers(d); err != nil { - return fmt.Errorf("device: stop stream: %s", err) + if err := v4l2.UnmapMemoryBuffers(d); err != nil { + return fmt.Errorf("device: stop: %s", err) } - if err := v4l2.StopStreamLoop(d); err != nil { - return fmt.Errorf("device: stop stream: %w", err) + if err := v4l2.StreamOff(d); err != nil { + return fmt.Errorf("device: stop: %w", err) } return nil } + +func (d *Device) startStreamLoop(ctx context.Context) error { + if err := v4l2.StreamOn(d); err != nil { + return fmt.Errorf("stream loop: stream on: %w", err) + } + + + go func() { + defer close(d.output) + + fd := d.Fd() + ioMemType := d.MemIOType() + bufType := d.BufferType() + + for { + select { + // handle stream capture (read from driver) + case <-v4l2.WaitForRead(d): + //TODO add better error-handling, for now just panic + buff, err := v4l2.CaptureBuffer(fd, ioMemType, bufType) + if err != nil { + panic(fmt.Errorf("stream loop: buffer capture: %s", err).Error()) + } + + select { + case d.output <-d.Buffers()[buff.Index][:buff.BytesUsed]: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } + }() + + return nil +} \ No newline at end of file diff --git a/v4l2/device/device_config.go b/device/device_config.go similarity index 51% rename from v4l2/device/device_config.go rename to device/device_config.go index e9aabdc..c5dae8b 100644 --- a/v4l2/device/device_config.go +++ b/device/device_config.go @@ -4,35 +4,48 @@ import ( "github.com/vladimirvivien/go4vl/v4l2" ) -type Config struct { +type config struct { ioType v4l2.IOType pixFormat v4l2.PixFormat bufSize uint32 fps uint32 + bufType uint32 } -type Option func(*Config) +type Option func(*config) func WithIOType(ioType v4l2.IOType) Option { - return func(o *Config) { + return func(o *config) { o.ioType = ioType } } func WithPixFormat(pixFmt v4l2.PixFormat) Option { - return func(o *Config) { + return func(o *config) { o.pixFormat = pixFmt } } func WithBufferSize(size uint32) Option { - return func(o *Config) { + return func(o *config) { o.bufSize = size } } func WithFPS(fps uint32) Option { - return func(o *Config) { + return func(o *config) { o.fps = fps } +} + +func WithVideoCaptureEnabled() Option { + return func(o *config) { + o.bufType = v4l2.BufTypeVideoCapture + } +} + +func WithVideoOutputEnabled() Option { + return func(o *config) { + o.bufType = v4l2.BufTypeVideoOutput + } } \ No newline at end of file diff --git a/device/doc.go b/device/doc.go new file mode 100644 index 0000000..51e144d --- /dev/null +++ b/device/doc.go @@ -0,0 +1,2 @@ +// Package device provides a device abstraction that supports video streaming. +package device \ No newline at end of file diff --git a/v4l2/device/list.go b/device/list.go similarity index 100% rename from v4l2/device/list.go rename to device/list.go diff --git a/v4l2/device/list_test.go b/device/list_test.go similarity index 100% rename from v4l2/device/list_test.go rename to device/list_test.go diff --git a/examples/capture/capture.go b/examples/capture/capture.go index 4d572cd..9ed8322 100644 --- a/examples/capture/capture.go +++ b/examples/capture/capture.go @@ -7,8 +7,8 @@ import ( "log" "os" + "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) func main() { @@ -59,7 +59,7 @@ func main() { log.Fatalf("device does not support any of %#v", preferredFmts) } log.Printf("Found preferred fmt: %s", fmtDesc) - frameSizes, err := v4l2.GetFormatFrameSizes(device.FileDescriptor(), fmtDesc.PixelFormat) + frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat) if err!=nil{ log.Fatalf("failed to get framesize info: %s", err) } @@ -96,8 +96,7 @@ func main() { // start stream ctx, cancel := context.WithCancel(context.TODO()) - frameChan, err := device.StartStream(ctx) - if err != nil { + if err := device.Start(ctx); err != nil { log.Fatalf("failed to stream: %s", err) } @@ -106,7 +105,7 @@ func main() { totalFrames := 10 count := 0 log.Printf("Capturing %d frames at %d fps...", totalFrames, fps) - for frame := range frameChan { + for frame := range device.GetOutput() { fileName := fmt.Sprintf("capture_%d.jpg", count) file, err := os.Create(fileName) if err != nil { @@ -128,7 +127,7 @@ func main() { } cancel() // stop capture - if err := device.StopStream(); err != nil { + if err := device.Stop(); err != nil { fmt.Println(err) os.Exit(1) } diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index 644a31a..c2134ca 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -7,8 +7,8 @@ import ( "os" "strings" + device2 "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) var template = "\t%-24s : %s\n" @@ -27,7 +27,7 @@ func main() { os.Exit(0) } - device, err := device.Open(devName) + device, err := device2.Open(devName) if err != nil { log.Fatal(err) } @@ -64,12 +64,12 @@ func main() { } func listDevices() error { - paths, err := device.GetAllDevicePaths() + paths, err := device2.GetAllDevicePaths() if err != nil { return err } for _, path := range paths { - dev, err := device.Open(path) + dev, err := device2.Open(path) if err != nil { log.Print(err) continue @@ -101,17 +101,17 @@ func listDevices() error { continue } - fmt.Printf("Device [%s]: %s: %s\n", path, card, busInfo) + fmt.Printf("v4l2Device [%s]: %s: %s\n", path, card, busInfo) } return nil } -func printDeviceDriverInfo(dev *device.Device) error { +func printDeviceDriverInfo(dev *device2.Device) error { caps := dev.Capability() // print driver info - fmt.Println("Device Info:") + fmt.Println("v4l2Device Info:") fmt.Printf(template, "Driver name", caps.Driver) fmt.Printf(template, "Card name", caps.Card) fmt.Printf(template, "Bus info", caps.BusInfo) @@ -123,7 +123,7 @@ func printDeviceDriverInfo(dev *device.Device) error { fmt.Printf("\t\t%s\n", desc.Desc) } - fmt.Printf("\t%-16s : %0x\n", "Device capabilities", caps.Capabilities) + fmt.Printf("\t%-16s : %0x\n", "v4l2Device capabilities", caps.Capabilities) for _, desc := range caps.GetDeviceCapDescriptions() { fmt.Printf("\t\t%s\n", desc.Desc) } @@ -131,7 +131,7 @@ func printDeviceDriverInfo(dev *device.Device) error { return nil } -func printVideoInputInfo(dev *device.Device) error { +func printVideoInputInfo(dev *device2.Device) error { // first get current input index, err := dev.GetVideoInputIndex() if err != nil { @@ -152,7 +152,7 @@ func printVideoInputInfo(dev *device.Device) error { return nil } -func printFormatInfo(dev *device.Device) error { +func printFormatInfo(dev *device2.Device) error { pixFmt, err := dev.GetPixFormat() if err != nil { return fmt.Errorf("video capture format: %w", err) @@ -194,14 +194,14 @@ func printFormatInfo(dev *device.Device) error { return printFormatDesc(dev) } -func printFormatDesc(dev *device.Device) error { +func printFormatDesc(dev *device2.Device) error { descs, err := dev.GetFormatDescriptions() if err != nil { return fmt.Errorf("format desc: %w", err) } fmt.Println("Supported formats:") for i, desc := range descs { - frmSizes, err := v4l2.GetFormatFrameSizes(dev.FileDescriptor(), desc.PixelFormat) + frmSizes, err := v4l2.GetFormatFrameSizes(dev.Fd(), desc.PixelFormat) if err != nil { return fmt.Errorf("format desc: %w", err) } @@ -215,7 +215,7 @@ func printFormatDesc(dev *device.Device) error { return nil } -func printCropInfo(dev *device.Device) error { +func printCropInfo(dev *device2.Device) error { crop, err := dev.GetCropCapability() if err != nil { return fmt.Errorf("crop capability: %w", err) @@ -242,7 +242,7 @@ func printCropInfo(dev *device.Device) error { return nil } -func printCaptureParam(dev *device.Device) error { +func printCaptureParam(dev *device2.Device) error { params, err := dev.GetStreamParam() if err != nil { return fmt.Errorf("stream capture param: %w", err) @@ -267,7 +267,7 @@ func printCaptureParam(dev *device.Device) error { } -func printOutputParam(dev *device.Device) error { +func printOutputParam(dev *device2.Device) error { params, err := dev.GetStreamParam() if err != nil { return fmt.Errorf("stream output param: %w", err) diff --git a/examples/format/devfmt.go b/examples/format/devfmt.go index bcc1427..753e015 100644 --- a/examples/format/devfmt.go +++ b/examples/format/devfmt.go @@ -5,8 +5,8 @@ import ( "log" "strings" + device2 "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) func main() { @@ -31,10 +31,10 @@ func main() { fmtEnc = v4l2.PixelFmtYUYV } - device, err := device.Open( + device, err := device2.Open( devName, - device.WithPixFormat(v4l2.PixFormat{Width: uint32(width), Height: uint32(height), PixelFormat: fmtEnc, Field: v4l2.FieldNone}), - device.WithFPS(15), + device2.WithPixFormat(v4l2.PixFormat{Width: uint32(width), Height: uint32(height), PixelFormat: fmtEnc, Field: v4l2.FieldNone}), + device2.WithFPS(15), ) if err != nil { log.Fatalf("failed to open device: %s", err) diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index c551e40..1b7bfe6 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -11,13 +11,13 @@ import ( "strings" "time" + "github.com/vladimirvivien/go4vl/device" "github.com/vladimirvivien/go4vl/imgsupport" "github.com/vladimirvivien/go4vl/v4l2" - "github.com/vladimirvivien/go4vl/v4l2/device" ) var ( - frames <-chan []byte + frames chan []byte fps uint32 = 30 pixfmt v4l2.FourCCType ) @@ -157,15 +157,28 @@ func main() { // start capture ctx, cancel := context.WithCancel(context.TODO()) - f, err := device.StartStream(ctx) - if err != nil { + if err := device.Start(ctx); err != nil { log.Fatalf("stream capture: %s", err) } defer func() { cancel() device.Close() }() - frames = f // make frames available. + + // buffer captured video stream into a local channel of bytes + frames = make(chan []byte, 1024) + go func() { + defer close(frames) + for { + select { + case frame := <-device.GetOutput(): + frames <- frame + case <-ctx.Done(): + return + } + } + }() + log.Println("device capture started, frames available") log.Printf("starting server on port %s", port) diff --git a/imgsupport/converters.go b/imgsupport/converters.go index 26f2834..c90a28c 100644 --- a/imgsupport/converters.go +++ b/imgsupport/converters.go @@ -2,6 +2,7 @@ package imgsupport import ( "bytes" + "fmt" "image" "image/jpeg" ) @@ -9,6 +10,9 @@ import ( // Yuyv2Jpeg attempts to convert the YUYV image using Go's built-in // YCbCr encoder func Yuyv2Jpeg(width, height int, frame []byte) ([]byte, error) { + if true { + return nil, fmt.Errorf("unsupported") + } //size := len(frame) ycbr := image.NewYCbCr(image.Rect(0, 0, width, height), image.YCbCrSubsampleRatio422) diff --git a/v4l2/streaming.go b/v4l2/streaming.go index e2b5235..fc1c06d 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -123,9 +123,9 @@ type PlaneInfo struct { // StreamOn requests streaming to be turned on for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOn(dev Device) error { +func StreamOn(dev StreamingDevice) error { bufType := dev.BufferType() - if err := send(dev.FileDescriptor(), C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_STREAMON, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream on: %w", err) } return nil @@ -134,9 +134,9 @@ func StreamOn(dev Device) error { // StreamOff requests streaming to be turned off for // capture (or output) that uses memory map, user ptr, or DMA buffers. // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-streamon.html -func StreamOff(dev Device) error { +func StreamOff(dev StreamingDevice) error { bufType := dev.BufferType() - if err := send(dev.FileDescriptor(), C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_STREAMOFF, uintptr(unsafe.Pointer(&bufType))); err != nil { return fmt.Errorf("stream off: %w", err) } return nil @@ -145,7 +145,7 @@ func StreamOff(dev Device) error { // InitBuffers sends buffer allocation request to initialize buffer IO // for video capture or video output when using either mem map, user pointer, or DMA buffers. // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-reqbufs.html#vidioc-reqbufs -func InitBuffers(dev Device) (RequestBuffers, error) { +func InitBuffers(dev StreamingDevice) (RequestBuffers, error) { if dev.MemIOType() != IOTypeMMAP && dev.MemIOType() != IOTypeDMABuf { return RequestBuffers{}, fmt.Errorf("request buffers: %w", ErrorUnsupported) } @@ -154,7 +154,7 @@ func InitBuffers(dev Device) (RequestBuffers, error) { req._type = C.uint(dev.BufferType()) req.memory = C.uint(dev.MemIOType()) - if err := send(dev.FileDescriptor(), C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_REQBUFS, uintptr(unsafe.Pointer(&req))); err != nil { return RequestBuffers{}, fmt.Errorf("request buffers: %w", err) } @@ -163,21 +163,21 @@ func InitBuffers(dev Device) (RequestBuffers, error) { // GetBuffer retrieves buffer info for allocated buffers at provided index. // This call should take place after buffers are allocated with RequestBuffers (for mmap for instance). -func GetBuffer(dev Device, index uint32) (Buffer, error) { +func GetBuffer(dev StreamingDevice, index uint32) (Buffer, error) { var v4l2Buf C.struct_v4l2_buffer v4l2Buf._type = C.uint(dev.BufferType()) v4l2Buf.memory = C.uint(dev.MemIOType()) v4l2Buf.index = C.uint(index) - if err := send(dev.FileDescriptor(), C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { + if err := send(dev.Fd(), C.VIDIOC_QUERYBUF, uintptr(unsafe.Pointer(&v4l2Buf))); err != nil { return Buffer{}, fmt.Errorf("query buffer: %w", err) } return makeBuffer(v4l2Buf), nil } -// MapMemoryBuffer creates a local buffer mapped to the address space of the device specified by fd. -func MapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { +// mapMemoryBuffer creates a local buffer mapped to the address space of the device specified by fd. +func mapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { data, err := sys.Mmap(int(fd), offset, len, sys.PROT_READ|sys.PROT_WRITE, sys.MAP_SHARED) if err != nil { return nil, fmt.Errorf("map memory buffer: %w", err) @@ -185,8 +185,8 @@ func MapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { return data, nil } -// MakeMappedBuffers creates mapped memory buffers for specified buffer count of device. -func MakeMappedBuffers(dev Device)([][]byte, error) { +// MapMemoryBuffers creates mapped memory buffers for specified buffer count of device. +func MapMemoryBuffers(dev StreamingDevice)([][]byte, error) { bufCount := int(dev.BufferCount()) buffers := make([][]byte, bufCount) for i := 0; i < bufCount; i++ { @@ -195,9 +195,11 @@ func MakeMappedBuffers(dev Device)([][]byte, error) { return nil, fmt.Errorf("mapped buffers: %w", err) } + // TODO check buffer flags for errors etc + offset := buffer.Info.Offset length := buffer.Length - mappedBuf, err := MapMemoryBuffer(dev.FileDescriptor(), int64(offset), int(length)) + mappedBuf, err := mapMemoryBuffer(dev.Fd(), int64(offset), int(length)) if err != nil { return nil, fmt.Errorf("mapped buffers: %w", err) } @@ -206,21 +208,21 @@ func MakeMappedBuffers(dev Device)([][]byte, error) { return buffers, nil } -// UnmapMemoryBuffer removes the buffer that was previously mapped. -func UnmapMemoryBuffer(buf []byte) error { +// unmapMemoryBuffer removes the buffer that was previously mapped. +func unmapMemoryBuffer(buf []byte) error { if err := sys.Munmap(buf); err != nil { return fmt.Errorf("unmap memory buffer: %w", err) } return nil } -// UnmapBuffers unmaps all mapped memory buffer for device -func UnmapBuffers(dev Device) error { +// UnmapMemoryBuffers unmaps all mapped memory buffer for device +func UnmapMemoryBuffers(dev StreamingDevice) error { if dev.Buffers() == nil { return fmt.Errorf("unmap buffers: uninitialized buffers") } for i := 0; i < len(dev.Buffers()); i++ { - if err := UnmapMemoryBuffer(dev.Buffers()[i]); err != nil { + if err := unmapMemoryBuffer(dev.Buffers()[i]); err != nil { return fmt.Errorf("unmap buffers: %w", err) } } @@ -261,22 +263,18 @@ func DequeueBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) { return makeBuffer(v4l2Buf), nil } -// CaptureFrame captures a frame buffer from the device -func CaptureFrame(dev Device) ([]byte, error) { - bufInfo, err := DequeueBuffer(dev.FileDescriptor(), dev.MemIOType(), dev.BufferType()) +// CaptureBuffer captures a frame buffer from the device +func CaptureBuffer(fd uintptr, ioType IOType, bufType BufType) (Buffer, error) { + bufInfo, err := DequeueBuffer(fd, ioType, bufType) if err != nil { - return nil, fmt.Errorf("capture frame: dequeue: %w", err) - } - // assert dequeued buffer is in proper range - if !(bufInfo.Index < dev.BufferCount()) { - return nil, fmt.Errorf("capture frame: buffer with unexpected index: %d (out of %d)", bufInfo.Index, dev.BufferCount()) + return Buffer{}, fmt.Errorf("capture frame: dequeue: %w", err) } // requeue/clear used buffer, prepare for next read - if _, err := QueueBuffer(dev.FileDescriptor(), dev.MemIOType(), dev.BufferType(), bufInfo.Index); err != nil { - return nil, fmt.Errorf("capture frame: queue: %w", err) + if _, err := QueueBuffer(fd, ioType, bufType, bufInfo.Index); err != nil { + return Buffer{}, fmt.Errorf("capture frame: queue: %w", err) } // return captured buffer - return dev.Buffers()[bufInfo.Index][:bufInfo.BytesUsed], nil + return bufInfo, nil } diff --git a/v4l2/streaming_loop.go b/v4l2/streaming_loop.go index 67171a9..c087419 100644 --- a/v4l2/streaming_loop.go +++ b/v4l2/streaming_loop.go @@ -1,47 +1,13 @@ package v4l2 import ( - "context" "fmt" sys "golang.org/x/sys/unix" ) -// StartStreamLoop issue a streaming request for the device and sets up -// a loop to capture incoming buffers from the device. -func StartStreamLoop(ctx context.Context, dev Device) (chan []byte, error) { - if err := StreamOn(dev); err != nil { - return nil, fmt.Errorf("stream loop: driver stream on: %w", err) - } - - dataChan := make(chan []byte, dev.BufferCount()) - - go func() { - defer close(dataChan) - for { - select { - case <-WaitForRead(dev): - //TODO add better error-handling, for now just panic - frame, err := CaptureFrame(dev) - if err != nil { - panic(fmt.Errorf("stream loop: frame capture: %s", err).Error()) - } - select { - case dataChan <-frame: - case <-ctx.Done(): - return - } - case <-ctx.Done(): - return - } - } - }() - - return dataChan, nil -} - // StopStreamLoop unmaps allocated IO memory and signal device to stop streaming -func StopStreamLoop(dev Device) error { +func StopStreamLoop(dev StreamingDevice) error { if dev.Buffers() == nil { return fmt.Errorf("stop loop: failed to stop loop: buffers uninitialized") } @@ -55,7 +21,7 @@ func StopStreamLoop(dev Device) error { func WaitForRead(dev Device) <-chan struct{} { sigChan := make(chan struct{}) - fd := dev.FileDescriptor() + fd := dev.Fd() go func() { defer close(sigChan) diff --git a/v4l2/types.go b/v4l2/types.go index 39cc4d2..7c3dfb0 100644 --- a/v4l2/types.go +++ b/v4l2/types.go @@ -1,11 +1,39 @@ package v4l2 +import ( + "context" +) + +// Device is the base interface for a v4l2 device type Device interface { Name() string - FileDescriptor() uintptr + Fd() uintptr Capability() Capability + MemIOType() IOType + GetOutput() <-chan []byte + SetInput(<-chan []byte) + Close() error +} + +// StreamingDevice represents device that supports streaming IO +// via mapped buffer sharing. +type StreamingDevice interface { + Device Buffers() [][]byte BufferType() BufType BufferCount() uint32 - MemIOType() IOType + Start(context.Context) error + Stop() error } + +//// CaptureDevice represents a device that captures video from an underlying device +//type CaptureDevice interface { +// StreamingDevice +// StartCapture(context.Context) (<-chan []byte, error) +//} +// +//// OutputDevice represents a device that can output video data to an underlying device driver +//type OutputDevice interface { +// StreamingDevice +// StartOutput(context.Context, chan<- []byte) error +//} \ No newline at end of file From 94af4eafb7887e6500a672d6cdbc2fedac9eb5a8 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sat, 30 Apr 2022 07:37:10 -0400 Subject: [PATCH 06/11] Device API refactor; documentation and example updates --- README.md | 32 ++++++------ device/device.go | 17 +++--- examples/capture0/capture0.go | 64 +++++++++++++++++++++++ examples/{capture => capture1}/capture.go | 0 v4l2/streaming_loop.go | 14 ----- 5 files changed, 89 insertions(+), 38 deletions(-) create mode 100644 examples/capture0/capture0.go rename examples/{capture => capture1}/capture.go (100%) diff --git a/README.md b/README.md index c1dfe7d..c0f90bd 100644 --- a/README.md +++ b/README.md @@ -11,47 +11,47 @@ It hides all the complexities of working with V4L2 and provides idiomatic Go typ ## Features * Capture and control video data from your Go programs -* Idiomatic Go API for device access and video capture -* Use cgo-generated types for correct data representation in Go -* Use familiar types such as channels to stream video data +* Idiomatic Go types such as channels to access and stream video data * Exposes device enumeration and information * Provides device capture control * Access to video format information -* Streaming support using memory map (other methods coming later) +* Streaming users zero-copy IO using memory mapped buffers -### Not working/supported yet -* Inherent support for video output -* Only support MMap memory stream (user pointers, DMA not working) -* Device control not implemented yet - -## Prerequisites +## Compilation Requirements * Go compiler/tools -* Linux OS (32- or 64-bit) * Kernel minimum v5.10.x * A locally configured C compiler (i.e. gcc) * Header files for V4L2 (i.e. /usr/include/linux/videodev2.h) -All examples have been tested using a Rasperry PI 3, running 32-bit Raspberry PI OS. +All examples have been tested using a Raspberry PI 3, running 32-bit Raspberry PI OS. The package should work with no problem on your 64-bit Linux OS. ## Getting started + +### System upgrade + To avoid issues with old header files on your machine, upgrade your system to pull down the latest OS packages -with something similar to the following (follow directions for your system to properly upgrade): +with something similar to the following (follow directions for your system for proper upgrade): ```shell sudo apt update sudo apt full-upgrade ``` -To include `go4vl` in your own code, pull the package +### Using the go4vl package + +To include `go4vl` in your own code, `go get` the package: ```bash go get github.com/vladimirvivien/go4vl/v4l2 ``` -## Examples +## Video capture example + The following is a simple example that captures video data from an attached camera device to -and saves them as JPEG files. The example assumes the attached device supports JPEG (MJPEG) output format inherently. +and saves the captured frames as JPEG files. + +The example assumes the attached device supports JPEG (MJPEG) output format inherently. ```go package main diff --git a/device/device.go b/device/device.go index c614956..d475909 100644 --- a/device/device.go +++ b/device/device.go @@ -365,13 +365,16 @@ func (d *Device) Start(ctx context.Context) error { } func (d *Device) Stop() error { - d.streaming = false + if !d.streaming { + return nil + } if err := v4l2.UnmapMemoryBuffers(d); err != nil { - return fmt.Errorf("device: stop: %s", err) + return fmt.Errorf("device: stop: %w", err) } if err := v4l2.StreamOff(d); err != nil { return fmt.Errorf("device: stop: %w", err) } + d.streaming = false return nil } @@ -395,15 +398,13 @@ func (d *Device) startStreamLoop(ctx context.Context) error { //TODO add better error-handling, for now just panic buff, err := v4l2.CaptureBuffer(fd, ioMemType, bufType) if err != nil { - panic(fmt.Errorf("stream loop: buffer capture: %s", err).Error()) + panic(fmt.Errorf("stream loop: capture buffer: %s", err).Error()) } - select { - case d.output <-d.Buffers()[buff.Index][:buff.BytesUsed]: - case <-ctx.Done(): - return - } + d.output <-d.Buffers()[buff.Index][:buff.BytesUsed] + case <-ctx.Done(): + d.Stop() return } } diff --git a/examples/capture0/capture0.go b/examples/capture0/capture0.go new file mode 100644 index 0000000..dfb258b --- /dev/null +++ b/examples/capture0/capture0.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + + "github.com/vladimirvivien/go4vl/device" + "github.com/vladimirvivien/go4vl/v4l2" +) + +func main() { + devName := "/dev/video0" + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.Parse() + + // open device + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) + if err != nil { + log.Fatalf("failed to open device: %s", err) + } + defer device.Close() + + // start stream + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to start stream: %s", err) + } + + // process frames from capture channel + totalFrames := 10 + count := 0 + log.Printf("Capturing %d frames...", totalFrames) + + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + if err != nil { + log.Printf("failed to create file %s: %s", fileName, err) + continue + } + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + log.Printf("Saved file: %s", fileName) + if err := file.Close(); err != nil { + log.Printf("failed to close file %s: %s", fileName, err) + } + count++ + if count >= totalFrames { + break + } + } + + stop() // stop capture + fmt.Println("Done.") + +} diff --git a/examples/capture/capture.go b/examples/capture1/capture.go similarity index 100% rename from examples/capture/capture.go rename to examples/capture1/capture.go diff --git a/v4l2/streaming_loop.go b/v4l2/streaming_loop.go index c087419..691f615 100644 --- a/v4l2/streaming_loop.go +++ b/v4l2/streaming_loop.go @@ -1,23 +1,9 @@ package v4l2 import ( - "fmt" - sys "golang.org/x/sys/unix" ) -// StopStreamLoop unmaps allocated IO memory and signal device to stop streaming -func StopStreamLoop(dev StreamingDevice) error { - if dev.Buffers() == nil { - return fmt.Errorf("stop loop: failed to stop loop: buffers uninitialized") - } - - if err := StreamOff(dev); err != nil { - return fmt.Errorf("stop loop: stream off: %w", err) - } - return nil -} - func WaitForRead(dev Device) <-chan struct{} { sigChan := make(chan struct{}) From 5395a936ba95f2eabdf0df1cba9d8399004a87bc Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sat, 21 May 2022 09:49:01 -0400 Subject: [PATCH 07/11] Doc updates for examples --- examples/README.md | 6 + examples/capture0/README.md | 58 +++++++ examples/capture0/capture0.go | 2 +- examples/capture1/README.md | 119 +++++++++++++ examples/capture1/{capture.go => capture1.go} | 0 examples/device_info/README.md | 40 +++++ examples/webcam/README.md | 162 ++++++++++++++++++ examples/webcam/webcam.go | 34 +--- 8 files changed, 393 insertions(+), 28 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/capture0/README.md create mode 100644 examples/capture1/README.md rename examples/capture1/{capture.go => capture1.go} (100%) create mode 100644 examples/device_info/README.md create mode 100644 examples/webcam/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..5242406 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,6 @@ +# Examples + +* [capture0](./capture0) - simple capture example with hardcoded pixel format +* [capture1](./capture1) - capture example with preferred format search +* [device_info](./device_info) - uses go4vl to retrieve device and format info +* [webcam](./webcam) - use go4vl to build a working webcam example \ No newline at end of file diff --git a/examples/capture0/README.md b/examples/capture0/README.md new file mode 100644 index 0000000..ef19369 --- /dev/null +++ b/examples/capture0/README.md @@ -0,0 +1,58 @@ +# Capture example + +This example shows how to use the `go4vl` API to create a simple program that captures a video frames from an attached input (camera) device. + +Firstly, the source code opens a device, `devName`, with a hard-coded pixel format (MPEG) and size. If the device does not support +the specified format, the open operation will fail, returning an error. + +```go +func main() { + devName := "/dev/video0" + // open device + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) +... +} +``` + +Next, the source code calls the `device.Start` method to start the input (capture) process. + +```go +func main() { +... + // start stream + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to start stream: %s", err) + } +... + +} +``` + +Once the device starts, the code sets up a loop capture incoming video frame buffer from the input device and save each +frame to a local file. + +```go +func main() { +... + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + ... + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + + if err := file.Close(); err != nil { + log.Printf("failed to close file %s: %s", fileName, err) + } + } + +} +``` + +> See the full source code [here](./capture0.go). \ No newline at end of file diff --git a/examples/capture0/capture0.go b/examples/capture0/capture0.go index dfb258b..89ab9e4 100644 --- a/examples/capture0/capture0.go +++ b/examples/capture0/capture0.go @@ -58,7 +58,7 @@ func main() { } } - stop() // stop capture + stop() // stop capture fmt.Println("Done.") } diff --git a/examples/capture1/README.md b/examples/capture1/README.md new file mode 100644 index 0000000..55e39f0 --- /dev/null +++ b/examples/capture1/README.md @@ -0,0 +1,119 @@ +# Capture example + +In this capture example, the source code uses a more advanced approach (compared to [capture0/capture0.go](../capture0/capture0.go)) where it +leverages the go4vl device description API to ensure that the device supports the selected preferred format and size. + +First, the source code opens the device with `device.Open` function call. Unlike in the [previous example](../capture0/capture0.go), the call to +`Open` omits the pixel format option. + +```go +func main() { + devName := "/dev/video0" + device, err := device.Open(devName) + if err != nil { + log.Fatalf("failed to open device: %s", err) + } + defer device.Close() +} +``` + +Next, the source code defines a function that is used to search formats supported by the device. + +```go +func main() { +... + findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription { + for _, desc := range fmts { + if desc.PixelFormat == pixEncoding{ + return &desc + } + } + return nil + } +} +``` + +Next, the code enumerates the formats supported by the device, `device.GetFormatDescriptions`, and used the search function +to test whether the device support one of several preferred formats. + +```go +func main() { +... + fmtDescs, err := device.GetFormatDescriptions() + if err != nil{ + log.Fatal("failed to get format desc:", err) + } + + // search for preferred formats + preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV} + var fmtDesc *v4l2.FormatDescription + for _, preferredFmt := range preferredFmts{ + fmtDesc = findPreferredFmt(fmtDescs, preferredFmt) + if fmtDesc != nil { + break + } + } +} +``` + +Next, if one of the preferred formats is found, then it is assigned to `fmtDesc`. The next step is to search the device +for an appropriate supported dimension (640x480) for the selected format which is stored in `frmSize`. + +```go +func main() { +... + frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat) + + // select size 640x480 for format + var frmSize v4l2.FrameSizeEnum + for _, size := range frameSizes { + if size.Size.MinWidth == 640 && size.Size.MinHeight == 480 { + frmSize = size + break + } + } +} +``` + +At this point, the device can be assigned the selected pixel format and its associated size. + +```go +func main() { +... + if err := device.SetPixFormat(v4l2.PixFormat{ + Width: frmSize.Size.MinWidth, + Height: frmSize.Size.MinHeight, + PixelFormat: fmtDesc.PixelFormat, + Field: v4l2.FieldNone, + }); err != nil { + log.Fatalf("failed to set format: %s", err) + } +} +``` + +Finally, the device can be started and the streaming buffers can be captured: + +```go +fun main() { +... + if err := device.Start(ctx); err != nil { + log.Fatalf("failed to stream: %s", err) + } + + for frame := range device.GetOutput() { + fileName := fmt.Sprintf("capture_%d.jpg", count) + file, err := os.Create(fileName) + if err != nil { + log.Printf("failed to create file %s: %s", fileName, err) + continue + } + if _, err := file.Write(frame); err != nil { + log.Printf("failed to write file %s: %s", fileName, err) + continue + } + ... + } +} +``` + +> See source code [here](./capture1.go). \ No newline at end of file diff --git a/examples/capture1/capture.go b/examples/capture1/capture1.go similarity index 100% rename from examples/capture1/capture.go rename to examples/capture1/capture1.go diff --git a/examples/device_info/README.md b/examples/device_info/README.md new file mode 100644 index 0000000..cc488a8 --- /dev/null +++ b/examples/device_info/README.md @@ -0,0 +1,40 @@ +# Device info example + +The example in this directory showcases `go4vl` support for device information. For instance, the following function +prints driver information + +```go +func main() { + devName := '/dev/video0' + device, err := device2.Open(devName) + if err := printDeviceDriverInfo(device); err != nil { + log.Fatal(err) + } +} + +func printDeviceDriverInfo(dev *device.Device) error { + caps := dev.Capability() + + // print driver info + fmt.Println("v4l2Device Info:") + fmt.Printf(template, "Driver name", caps.Driver) + fmt.Printf(template, "Card name", caps.Card) + fmt.Printf(template, "Bus info", caps.BusInfo) + + fmt.Printf(template, "Driver version", caps.GetVersionInfo()) + + fmt.Printf("\t%-16s : %0x\n", "Driver capabilities", caps.Capabilities) + for _, desc := range caps.GetDriverCapDescriptions() { + fmt.Printf("\t\t%s\n", desc.Desc) + } + + fmt.Printf("\t%-16s : %0x\n", "v4l2Device capabilities", caps.Capabilities) + for _, desc := range caps.GetDeviceCapDescriptions() { + fmt.Printf("\t\t%s\n", desc.Desc) + } + + return nil +} +``` + +> See the [complete example](./devinfo.go) and all available device information from go4vl. \ No newline at end of file diff --git a/examples/webcam/README.md b/examples/webcam/README.md new file mode 100644 index 0000000..ee86efc --- /dev/null +++ b/examples/webcam/README.md @@ -0,0 +1,162 @@ +# Webcam example + +The webcam examples shows how the `go4vl` API can be used to create a webcam that streams incoming video frames from an attached camera to a web page. The code sets up a web server that returns a web page with an image element that continuously stream the captured video from the camera. + +## Running the example +Keep in mind that this code can only run on systems with the Linux operating system. +Before you can build and run the code, you must satisfy the following prerequisites. + +### Pre-requisites + +* Go compiler/tools +* Linux OS (32- or 64-bit) +* Kernel minimum v5.10.x or higher +* A locally configured C compiler (i.e. gcc) +* Header files for V4L2 (i.e. /usr/include/linux/videodev2.h) +* A video camera (with support for Video for Linux API) + +If you are running a system that has not been upgraded in a while, ensure to issue the following commands: + +``` +sudo apt update +sudo apt full-upgrade +``` + +This example has been tested using a Raspberry Pi 3 running 32-bit Linux, with kernel version 5.14, with cheap USB video camera attached. + +### Build and run + +From within this directory, build with the following command: + +``` +go build -o webcam webcam.go +``` + +Once built, you can start the webcam with the following command (and output as shown): + +``` + ./webcam + +2022/05/21 09:04:31 device [/dev/video0] opened +2022/05/21 09:04:31 device info: driver: uvcvideo; card: HDM Webcam USB: HDM Webcam USB; bus info: usb-3f980000.usb-1.5 +2022/05/21 09:04:31 Current format: Motion-JPEG [1920x1080]; field=any; bytes per line=0; size image=0; colorspace=Default; YCbCr=Default; Quant=Default; XferFunc=Default +2022/05/21 09:04:31 device capture started, frames available +2022/05/21 09:04:31 starting server on port :9090 +2022/05/21 09:04:31 use url path /webcam +``` + +Next, point your browser to your machine's address and shown port (i.e. `http://198.162.100.20:9090`). You should see a webpage with the streaming image in the middle (see below.) + +The webcam program offers several CLI arguments that you can use to configure the webcam: + +``` +./webcam --help +Usage of ./webcam: + -d string + device name (path) (default "/dev/video0") + -f string + pixel format (default "mjpeg") + -h int + capture height (default 1080) + -p string + webcam service port (default ":9090") + -r int + frames per second (fps) (default 30) + -w int + capture width (default 1920) +``` + +## The source code +The following code walkthrough illustrates how simple it is to create programs that can stream video using the `go4vl` project. + +Firstly, the `main` function opens the video device with a set of specified configurations (from CLI flags): + +```go +var frames <-chan []byte + +func main() { + port := ":9090" + devName := "/dev/video0" + frameRate := 30 + + // create device + device, err := device.Open(devName, + device.WithIOType(v4l2.IOTypeMMAP), + device.WithPixFormat(v4l2.PixFormat{PixelFormat: getFormatType(format), Width: uint32(width), Height: uint32(height)}), + device.WithFPS(uint32(frameRate)), + ) +} +``` + +Next, start the device and make the device stream available via package variable `frames`: + +```go +var frames <-chan []byte + +func main() { +... + ctx, cancel := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { + log.Fatalf("stream capture: %s", err) + } + defer func() { + cancel() + device.Close() + }() + + frames = device.GetOutput() + +} +``` + +The last major step is to start an HTTP server to serve the video buffers, as images, and the page for the webcam: + +```go +var frames <-chan []byte + +func main() { +... + + // setup http service + http.HandleFunc("/webcam", servePage) // returns an html page + http.HandleFunc("/stream", serveVideoStream) // returns video feed + if err := http.ListenAndServe(port, nil); err != nil { + log.Fatal(err) + } +} +``` + +The video captured from the camera is served at endpoint `/stream` (see source above) which is serviced by HTTP handler +function `serveVideoStream`. The function uses a content type of `multipart/x-mixed-replace`, with a separate boundary for +each image buffer, that is rendered on the browser as a video stream. + +```go +func serveVideoStream(w http.ResponseWriter, req *http.Request) { + // Start HTTP Response + const boundaryName = "Yt08gcU534c0p4Jqj0p0" + + // send multi-part header + w.Header().Set("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=%s", boundaryName)) + w.WriteHeader(http.StatusOK) + + for frame := range frames { + // start boundary + io.WriteString(w, fmt.Sprintf("--%s\n", boundaryName)) + io.WriteString(w, "Content-Type: image/jpeg\n") + io.WriteString(w, fmt.Sprintf("Content-Length: %d\n\n", len(frame))) + + if _, err := w.Write(frame); err != nil { + log.Printf("failed to write mjpeg image: %s", err) + return + } + + // close boundary + if _, err := io.WriteString(w, "\n"); err != nil { + log.Printf("failed to write boundary: %s", err) + return + } + } +} +``` + +> See the full source code [here](./webcam.go) \ No newline at end of file diff --git a/examples/webcam/webcam.go b/examples/webcam/webcam.go index 1b7bfe6..2e1d413 100644 --- a/examples/webcam/webcam.go +++ b/examples/webcam/webcam.go @@ -12,12 +12,11 @@ import ( "time" "github.com/vladimirvivien/go4vl/device" - "github.com/vladimirvivien/go4vl/imgsupport" "github.com/vladimirvivien/go4vl/v4l2" ) var ( - frames chan []byte + frames <-chan []byte fps uint32 = 30 pixfmt v4l2.FourCCType ) @@ -71,20 +70,13 @@ func serveVideoStream(w http.ResponseWriter, req *http.Request) { log.Printf("failed to write mjpeg image: %s", err) return } - case v4l2.PixelFmtYUYV: - data, err := imgsupport.Yuyv2Jpeg(640, 480, frame) - if err != nil { - log.Printf("failed to convert yuyv to jpeg: %s", err) - continue - } - if _, err := w.Write(data); err != nil { - log.Printf("failed to write yuyv image: %s", err) - return - } + default: + log.Printf("selected pixel format is not supported") } + // close boundary if _, err := io.WriteString(w, "\n"); err != nil { - log.Printf("failed to write bounday: %s", err) + log.Printf("failed to write boundary: %s", err) return } } @@ -165,22 +157,10 @@ func main() { device.Close() }() - // buffer captured video stream into a local channel of bytes - frames = make(chan []byte, 1024) - go func() { - defer close(frames) - for { - select { - case frame := <-device.GetOutput(): - frames <- frame - case <-ctx.Done(): - return - } - } - }() + // video stream + frames = device.GetOutput() log.Println("device capture started, frames available") - log.Printf("starting server on port %s", port) log.Println("use url path /webcam") From c9c9b9e15f16b89da09c7f883834b3676b126d08 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sat, 21 May 2022 10:10:18 -0400 Subject: [PATCH 08/11] Add image --- examples/webcam/README.md | 5 ++++- examples/webcam/screenshot.png | Bin 0 -> 184644 bytes 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 examples/webcam/screenshot.png diff --git a/examples/webcam/README.md b/examples/webcam/README.md index ee86efc..771edef 100644 --- a/examples/webcam/README.md +++ b/examples/webcam/README.md @@ -45,7 +45,10 @@ Once built, you can start the webcam with the following command (and output as s 2022/05/21 09:04:31 use url path /webcam ``` -Next, point your browser to your machine's address and shown port (i.e. `http://198.162.100.20:9090`). You should see a webpage with the streaming image in the middle (see below.) +Next, point your browser to your machine's address and shown port (i.e. `http://198.162.100.20:9090`). +You should see a webpage with the streaming video (see below.) + +![](./screenshot.png) The webcam program offers several CLI arguments that you can use to configure the webcam: diff --git a/examples/webcam/screenshot.png b/examples/webcam/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8dab9e1ce9af908a207240b019889530eab80eeb GIT binary patch literal 184644 zcmeFYWmp`|w=X(au;3wBu;3v`a0X2vxVyVeV6eem5+uRhU4wgY4el;saCaLV?&N*{ zXYc*obML3~^>jV`bXC_{wQ5zZs#^W4>QE&GNh}Of3;+OtB`qbc0stU*!tYD87w|WK z=ELC+1PPFsn3A-Z7?qNvodw9o8~|X9HZ?NBmwwOKZ)|L2)IY+^gyHC_5)u-oV&vP~ zF-SGs@!ROPQCyOq-YPNfDw0|kAXmAw!TMzw)m=4f|9dF(N?RW?yT^ch@&$Kx!lhR& z67iRc%8yw8OG+vQSysk(@KWUvs^f=bF+>lssdV$ub{VNWu|v|ajqLzcu~dgNM%>gN zMVpE301Jo%B%z|5#D;(cptRKbo|H{dD^F@ZsM`?yi z+HuJl$~miOFZ0z<##+pT$WdsJXlP!%P)ZV0^A*OymX!#l{ibfgA6|i%#|zL9K}x_&@O1u#FyiRnjbMkc}^-KCo9(+&n)|`M!7oLw*s| zzVr0_EVS|b+&)E2oYc(fbvfq0FXf-`>(hP0^@&Z5CMQ-5CHipNYj^n3-=p_8#ef1ya z|C#y!X8cE_)_;rS<>2|BBL7Fu|KR+q0#MNr1ea#~w+)3q2(tZe@BTwC$o98^|3`!W z+nfKqg?Fbgh9KL2nT9aNN7oW`06-KVE&fr>4dE~y)laXk?rH3Gh3AR)GIJ||2Ya>{ z8tx~Iyf$1ZH#W9(U#j@1yGa5mMkXh@6C)2)q=G=tljVcDhLPuc(DpEte5`ctn~^82 z=MI#aoLpM6DKjF%DL=-wa7=nZVU<*c$$F)r?&@S|N=8=JHQ-I(d)qSPFV+V((60RY zs`^;zq4e!N2M8ltdb?DycHt_CUsez8V zHz+yz7_;dd!@AP3L@{3LXKB1P$UP@lst@tcknCWYI*6GWPrva=Jx(2YoUNZ1&rf5e zQ5BUrR$^?(P->2*G&`I6;d7J}nmt2vKDq|lsX(or^|ZH>GNLl#5-S2Vr0dKC;c{kX z)?*TB_V|dmEITK@OyH8SWX02v883=nU#7#L>4xy&*D5P>Nkui*k>pbEplVs3yk*TD zkkk1!K!~c{q+6yL)CuNs?pCwborNkKIK&(+d*hT?7ZDD9I$ciES>l1N*~lDp801{u zK!|`rJvH|by5a6v2S-rM*tmOIo^QFKtQ*5d-@P6>;FNB*scv8IN}SLZ`y!gZTuDEu zE!7qJ8YO_l`=5$S!V8N=ogoyop4# zT~Ecw6zBc7NA1qm)>_YLEN<3K>PJud%uHaGB)dwLM_tVZsE^=AqKjoo@GuZImD=1{ zp6QypY@&&NdR?1xmlD7R@;kotOe-$n!gWih^LK$AF4UX+ma$$cFT*M>RZEBq2E6wF zAvtiz&(4RHi};{}xG$}<`9LW<#|C}h%ToTj~G{{H`|Ln#j#qH;V zL$FPP$l!YqCdP@+=;G*H@uG-_gjBfr)!IxQW0@U``(_r=vugF(K&#tTR^MVZG4^Gx zVqnON+Q&PkDRK1y5> zJNwg?`wY*cJqtBLgv%l+x%dfVkDK#!Z?bJq`^t-3jI`EL8!(Ft;ju3hsokJM|(&S)LC^(?ad zdD}f@WiqGbpl_f$`opuw?-j&If#a4XsQ0yg$;{7~h86d|pFd}V?;9_+7XnZ)!MEpI z`?%XL+J2vl>Hl$?Vp&jtZR^@%ahw7dRrqVa%nUBpSz8VzuyRYd{Lz#G`5XQ-Xv6WX zxHe85t!E0fjdM!9xH9-}e>^;n{8 zLUXfoJX$|ZDVd{3GKk8{vqwE#sb6RoGqRZiIJCb{7r%1j zP3?9^4N0|Y9C;e2LF_3yhU(@*9xyH-V#Rft43)%p{P}ORmFV+{m;2zM(Z}j|y(fW# zO0$pbHKoLL!RqN=DRB_cK`|G5qEw9~hs&BSXX*WZAIfTg2g2i|Fwu~>D${350k7m4 zsUMV3P|;)dP(ExXo@hXgc5kNeFl;&$R@KRXsfURq5cfsi`8kNo2DfXHgDOy9CF-Q)XAF@fmD3DtNeudji!U+d6w z2NJA-A;W7>_+n3SP_SOmYTfC((*uh`|0|vSv!BcrYC@$oGoJEVa0Gh2IV3Uoq^lyYrNC0l?P;!Yh1?E za%Kc5-&?MGr_5Zu#29WsHD_L=al|z_P()CBzk&M3ltuun!Tcb>YQ)Q$Y#Cgu0iPtB z;%@S&$;(rl3g+9S`Cuy%ItjO1Y=%ZiL1C54eUa6ny!w{SM?9l#j-B6TC3G?c=HB;3 zkekjKxCW*}Mho*h8|v1&gJrt)m|{n)_W`dL{ZT1!bFxSh;^P-zEF8Hmfm^)Km*M_xTbQ`FDI+lh$!9(@I$aBnW@Zo1n*SJ_fwL|LTXW+Y0UA z>PJa%VvL;ITF1SZIadaD^wg(vx@eopzt@`97)v~KHDVDua1!VO(QK4i43}f zv0@*6G}teUGWc$?0&NyA_js&lOS8^g35$$X6}YMJKt(Zqy(!_dhAHid40b`T)QZDa z8@=IJj&8cnbGee&S>HTwADcWzfT`_n$#w4vM>dH7SJPFdlFiQREF639>o3|Iec1Pu zT>o7|WT-^u2}YN;?1)K4eBJc}u&r37P@B0rI$w0yYUC>B{qC_9t_<$+_Ya+Bu4w8V z%y(dm2d$2aVb?>1FNy<(*a!}51pkb=-*G2LOucIIU{GdkiSp$r`^43p;;W)DaDMMC z+d-itVN_!|h5_M*Pj<6_+EW#%zI;%XpfVy$yr^-d9aHdiaztBhxq^|1k?IIE*2Ugv zesuG_4tcB#9}YyP9g*ueD|i&-y(!A)!^cbht9SRz9d_)Xzn6-xy|;!)RwE38>EIxqH{6$7x3I~Gm2!f zSMofbzb4h@Li|8Ed(+1)9W|Qhj0(nqoSs>>)BDqq6pjm>Il6BN3!Ip=@-Q^#$;~6P z^ovZb(RasOkF(>Ilxts3c*Vxaw?yLGqaThN;k4B7%6$IZn85S}`y*@1!&>21q*1+Eu{idhbtp2liFLHDf74ERbR&33HXXJuIA3T*21xk?zm!T~*Tew*1)LzWV z=1s0gX6XX^8?22*4JaR8%_v6NI$hG$f4KsBhn7V?O>Q*g#C&{j6W0NY5*@hP$J!(5 zKk5VvoQn#e9%}DAeH0*4vb78p@u5%1NJNYHL!!7LAJVkMW7aotVQee6;=(x6H9wc`f1RJUPL(d%8Wy#(hnn!{R37j$)}9 z@?cbM1L{aDPip#<;dl{SavmeEsro2vINRzee?D|ImRNsJS20R+y)OuP+_+GnL6F$nT2w93qa0XsD|CB)T<%jGgD$#- z*)Gvho$dE9Hj-=9TnAOwnbQfESy+#nUGsvt_TB~7h0>__}GsbEa6yVQmOtZXd z|Fj}rJL97~K=&&&4)e2YRL>Gil&D5Vs^23j79s##jdv<{azf*m@QOn_XwzGEy_$Ni z>1yU<>PifkZdY?Ymu|{V!nypUxht8x$Ln8XiJ4wLw>dkL6Rt9eV&9g&3Vz*k+v!!5 z7{nU#wvFH+*}lG=)U;cntr*E)^Ll8atEI5it;i!upgy?!n70!1;7K81W%yGtb;Z21 zn6BmRhy`6#yrl~)7NY~f-gszjhkP za{Jk~?!qsYP#pPqYmm+^q$lGp=a7*EOsLsLScxM#q?LmdXf(ttGzk+i9Mp|hB4X`mHJS0ZR#h-7SUNcRJECRAXcJ2Z3n z$JF+a7`4v&4N?8+R^&}a!o_CRg&BV_uZzl+eYsa{Vrr&n)Q+XB)rI)VTed;*wkZFT zOX)k-z61_})DF}ID$GPl*$sxh78csaUwxVNea-dxw?kWmnYyh0)s+3z_IB@GS0I_K z1z7F9Aj^KQthqPVJ0EmW9qmQYbRS>;Jk=AXa9zA4amj}PcUB|sDZI2$!)j;hJ1;&G z9!%y#crqK$+=GI7N82ACzclY>&gq;O2!W-xR^PirZWpvvOSZgIZ9_2)I}rW6?vgFR z(?!wQV()t}1w=YN3YzVj(-8EW?M(K_&^);D^OOAOR+~S`;0jetwtu_Ed#2|mOqZ+x zw54H5t&q?Bpf5?}WS65IjoJ}#LN@Q>x5nc&U!N>kiGo7afxxy|1^1Ta2d8wJl_sTQ zm2WDYQG#14l}XGuBC$3YX?)^F$c<$a-(!h>UXX71nEKm+3Y|guZ0GV+VaHgxKQIuc zSJ#Z;9rD{Ysk{LLs(~Et^G|E359uKS{_d1)}URpoX;jmwswKk{hMOB zn2&*GkEJP(RiIov>dCn*t*jl=sUOcZ|9}8V!sWFm9ogh(D2~u&lu+Ta#){X0{6XQ! z70`KI;M-kP&=jw4esuhN_xeTHB2b{`PT;d}Dr9dv+4iFUbV8C6>uRA{k+NjO#&k{a zmkOKL*&)vZ^2s)V3~ukzYiX)}PCubdwcSS?L9=TSu7S48TS)g0|8`C6YZixb*XgGd zPK%M*Dj#6dr~5X$QRTfnk=rfJ!yH*36U%gm6z~1Rnp(0g3YR*-y@1GLxc8c>r4Dbp zsCv;_v&fv`!(9W9ja0t=WxCxw_jkjRZpnE^V$CAK7Iw1QYwy5%@=?idOwU$@x~5A_ z-%3se0sgoTE3)E$RvuRDx@CK32gZc214dLWe{1v&BjBQrBfMe|Ixo69T9WKg#v^_3 zI;wQB&;Y1`<3nr)jT}y#!1Xvh}V452OE78dVavHFT{>o+wwghU^r4gD@iD}=?TF@ zcqw&lkdAWt=VELD6F7>eeZ4y&c2@C{L8qMvI1!5gvcd|76F*#|y&v=!%uz2L81~>?9kbCKVR)bMw_oV0J6w@Ej)Oax3 zjJO(eLS;D-|1;M-w;rPo0V=vayX(Yic!Hchg1Im)8%db?y^B*+BlKa3DKMiZ#Y zaM(^>{9GFxt%*H#7C{Id;h!u7Px54V+!ZVFdf&saj(RRs*xU9~PpBDr1^H4g*nICG z#3T-GxQ3r%eMncyE_17H6$gfPmzj~h2P z_C}}HPEA#!V!gqwzuVevGXRR`@b8?JKlQaAL3X?b`Thh*)VQ7Dr!(E>sfYIUDLxlD zAEZzoKGJ1VKc2B^?y42b2wZIvbYr^oQ==|n@j~%Q7jSO{z_vc~x)f89JHNXTq!hAD zbfIwE02+~MpVwL#`*SzMk*vJWT_>es?@by@$nWDYIz)OUIP$v{aK?4!Zh2Ncb;2eH1eXh`SDQg zN1%j?OObIY)lZO1DnaDeD@0n{O4G3~lGW}!o-piWF+HY2z7cFeDash@aLiv;qDO;$ z^q&Ug4Di5U#g(3C#HR@j{0%{|?(ERMCA@)2)8>t8MdIC>7|+S?UKJefDG zqX#eZ*?ff4xavO^YM%*uT<%7>av9gI48$@lp%}JO{h6w2+&el{%m9y-K%)uI#dQtY zef_VL8RU09`PNXFs;h%7 zI$M`D1Amd({^~TC3bi_Exu}<)q6RAN%9)#uW<1_5k{u+a%XSefpEL6oHR+9v3DX_P8qYbc;FDov;+?>yfN~ zb7i;K(0=WGe|}4}*t2wu22x?VO}mHK_dV{vr{8mH3#J$P5ce6w=T&2mCw$J40$uUb(nc$ImmN#=Va?2kQGqxw4dz*I z*$@04VO5yz5C?$J;E-(W0b|Av9Z67qm#oi*JOOw(N!cX^7;XLJ9N2mf_57GB)9cTR z)z2gwwgqllJ>-1c~=f-mOFLgQJ>9timFNilog@#BN=9o=3F67OfSRbV(mm>a4q^^_M-J8g%X2yEEL@rFDxS z5Xo=~!l&^j#mPN=t8ub;*N(#?8tmAG(uT|7xX~Sbu$}cN^2p6>Xu}IA9ABu=cUdrp z-LE`DT~Cs~)$;e6d#5@}~*`gUOs0i!8h=_}IRMNIApf*mFgz zna>Y}2#lLV7vuUsb(|$8xG~8vA{=3+fjU;VNKLMat$8&<&Fye4CzI;b}&A?BPCx_ zTC6;0K4YX@elrvj@grlSW&<}5YjkoIc#S^t7X^L~3bXakUO4brxR=fCR`Z7}o<`j| zh}e|%tWntlF#NVI-~KtOUZHe6{kRr|NgC8QP>D8nlj+oT2IMl6VW&L%)TH~C1r}rX z*bq*^&Ed9n#2$B|*VYeM+K}~-Hhg^p+n=HN<9awQ4A^UEyKP^~|8$SDuH3m+rBT27 znD34lMZf@k9kG&HJTXvdEQXYX^yJ5x@@m^}HBPvDCsskQQ(*!Vw$rZ1YVlh1l^-dC z=AVJ+O-vQm!_8a#V#_^nu zPGi!W`91|422phx)h#vF{ax6S-CVB|`0#Q{5r@hz{7sZL8@Q#7+Pj6S&>@{VO5kZe za(>L`2@}4o&K>!XAKVm*JTtu!d`>ld)ajErCaZ021fiBYNfOpsXPVBWOQ)7HIZypp30Gkt&QY3qTZKtBJ_mRN;?(A-| zA|$IMOR9=A?;nDVc4gIX^6-$;wZ$re>~GLX8EFXqOy=(ef(k(5n*4bj(+e}2`r!Nh zw6&{)l252yJ`XqJYvZ>3s|9u|J{|_PfpPRS1p6V%OMn@9yJRh$qbkcJ8aG)Tw-sS; z=K_d_tv{ZaX1LLS?i{Orx6l^LW)bJ$xbB7NG=KU5g$gz!rOS0i+H*XhV4QnLFb*bl zmy}vrNK>JZ&c!>Ww<$+_vg+pSl6RW}6xjYeTcqH&0$*W<{4s86FS=SIb9AD_X%}9g z$Ekd@!ZK`LZ86&MJYAFaQ1(C-UAv@x*G7ju5&_XC!FQcq`GiHu|4hh2HcplPiM6HU z3vM5|XV~MmtM9aIGd+Mh{6^8Y^^;|r0=B5)gPGXC)?Ax+^Ekh;OQ&-AW!>BP`9@T; z3;v*J>5*>_F(>XRX%EYkPc}ai-ebCV#i`{>c%2|c2Gv+ik!o7xMY+>dO6C&EC!HFn zhd1%~iLk8(PL@|pIfSGoW8O?MwY-l-!d}f3@+^Ne0JZ)!7SvfG#%k{iCnKj+wLy#XPxAZ#XvMvdQ*z`E@S=};gmcKmDIc(V^h{!OyUh!?$_k6@c z*p!GQVF4VF1D=IJ<8gsp<-Z=jvuA(*WS=;SM33F!95RZx{k4M3wu#>#XO#j+!QHGZy*E0h7S&kxF*V+&TMf zH513c24Z)Bm6P9}m0}Kk*JWqY*4k<@9D(VKiT*?Sg-He|Q2D56P{WE=LSU(5@YM`5 z`Acxzfa!V7hv`h0V*<_CLcu``aq0a*LxD;Luo(wX%1muJ&+|T~WjWYOU zajg(XE?Za9FWb=E^cSBMCB2@u*nF!>D%$7;yE%z=jR&m0PC%Sa+L60}p6@2X79x$8 zJ70Qo;lo$~D;yhOgKZS6o%bm_lhZZhFno%;N0>J4&1a-9=s2Xe?d7e|f|l}l_nd z#r+$l=;-EpE{F6)D1rhvcU=k>m@?wsA+7v%E=OV3W0Y%TFnvgP22sX<=tGs}Xbh7@ zxL;r{=0q0YB{1S65-O9xp7bbu-{o68&HUK}8XR9GvS|-v4|-YrDjj8^3o>7sz2^Rt zOCf_FT2UI-sq-#R(g#7#fKfs=j=`za0ApZe%_xxhtFmhxUJd?jmJfpK z;pMcaxp{tBf{Q#wh=5stloI`BXjoDp6}I`$$F?+-(I@P^_WE)|XMN)$kxbNqG2m6m8f@~y08x8z@+6ou1q zwrLYm3NA7tB%4kHB{?btd5cD+h@f$FLTp*)+3VpgYX^=DhW-4~Klb{jdNc1Y^XfVz zo>$XLF}tz@Ma(~9pKeBacKkq}uiEGhdWf+@H9U`w!eluWe#nQssq zLRCgb^T)&4B`#7SswPpadw^Zk!6aXyOF0A$hVE~(0oKUSt_-FoJdRIpS&8Y1fumR~ zYY0N47aiWhA`cC{b+jS?yLcdT+D`XH6q|gZbP(Ot*{|{2)(hAUEzSdOV87U%KfS8H z!VZ`4gR}5}pqq!Z!EINpkI%^ise9hi=ko;+d031)&3ow~@AHN3<*|wkRzZh7M;>_r zWV5cx>m>L-V28R={0as&&#SC%DrzTXqYD}iL~3mh2uopKj2P#5G(jgo?PuYUy3GEif~kH&e_Gz^=v3W0t^#7&O6hzy{YB0z^KVZdc4Q0lTPl6a6YE0~ z5?!e5uqDMqx>tVh`A}04Y+pBOMh5zFWLym8yZVkv+CJ#{;wYZmc1@vg?m47>-4TRC z#eC>I)Dkh+q;OmqqO~LM3-MZwjw~#c{_aj~yYf8^ip@-rl(%CL@zk@@TB~?Bx6-X-J0=$6 zcTfL7N)^0_R;Zgp+^|#GQ#!aMYv}%s~w8rjP&=W!1_jQ_3udzdW2za zt4F_4s2-9@_W=!ACh%8mWTCLaJhp{fjq!r$#U(*6&G?lWBm7oOjZgssf7~4pl2tk7nI@Td3l56;e2dZ~;j$Ish=#|5{Ln-{oA*vtTKmQJJ}XNN7uz+Y zNrH8{wQwn^k4t~bi>vp_V?+-8!=1+4R0h;0$eZ8Ikj{H7XnP0E6nj-Y^PuS$9=*O? zfS;vOi(H~SJ7LjY9F?OOCkhW8tPK9=+Zq*zj``sz-|<7>fl&oLt}pcDZP~5XA)`P1 zz(zTHm$pak(d7u(Ucz4W^>pPDko(*d=9PsXV)X3) zU4;J0M}`XX9Q{(SsS27dVStkgF}&TR=^#f@kle zUDA}w1It9yRMzsV)sk9<;U>}}i{f@H9lX9L*~1^x8hb9Kj5XzDb*QQm#JaKD_e_2L zo^w&6$oKlW6sAm$v{8Qs>LiLp+$VIJO}D=^`!2jlt=1sxM|TRO>pKGW6sqFhL>#Lb zXYMaSg@u250(=8v9^6KECQB!myl~rOKrJnKgBgZdWu=;Xn`J+BQWct<2XV;)KG-OE zn4(+;7suY(KV!RWW{uA3kNrx>BSG7Mw>l;*Zf6_Hr|+}^iO6qgweucV4`*Di;9vR) zx4;l&LIVLi7dGZ9jJ5Ysi$yiomeN8Op=&CBzcgx(29Fv{o0ly6u4)yw@<#$H3Pbat zTFPM864OSXQBqHNDoQ_n&}4G$oa-kr2MtQT$;>|W>_s>uXy49jjt5q!ynHwuyzIqwO;GBugE>MSjLquYuV$99ZSu#k7=U> z_WqaYkCz1fvF9F-;Bm@y^z`onz%&h+irjJ=J)z)8ikXQwQ-$e|`&m!*x()Sv;d8sp zf+{T%=r-Sw-_F~88#jp~S42ek#VPtc+~o|*X4px7xv9Yv+plBp>dso-kcBosjygEa z0TF8rKc4M97V)UWN)Qr3zwvqQJ^yn5hx6j_H{HyvERWk=Lj3o3AIzz(s6hi6$m?y2 zvqr=8VaXRR7zD#C$(!JhRd1!EWvBwYqctANHEig)5xPBhO6v(SaK~lEC>VRvf4JxQ zLF5~8QP#d<;opo-h#yyZ3(SWTybF|;9AJ81wk1+!R7UCzcqv8NLo>!f&O-N29|0@&f6bVXAB~Q@A}l z-&{E*!7jz^xXl55S(6-~m5X*fj?yBQ;(MRMzHx?{<}hPa`8ZBNf|o5C>C+wX{-~|> zuuJGZPjjmOr16{e5mkQZ0Zr^kjvxqMA|(lApX9)P)IY>WAMA9|nDJKYyS60h(=8eVcFvj)UPKv+rhFw3Ws9dEMV>yXaN47dsrTC96eS-NOpa!KGx6KUb64~_ zVvf`YD0H5Taa`6E>9nJjJ#fHV`BY=A8T}|Soq_H(a27BA7g5gohm8quMcQxG3 zSg;w(?Ns#{ZIN3xKnp=cA;Nb)oQ|2iJzq?|>*JZx7tW=XvpVVB9`~-b0o&_)C17di zbgU>Qi(HOm;g9Mru4lSGjYWx>Li%Da<`(()Xsn2xYQIu2Y%|$=%J1og8$x zQU&P7ULpANVSf!s`MrK_n{ zN{Djr;jC-o(j#Vxjn$H4YBbX2R~)spM#R0?+O93sdstnoCkNKzF81Lv)J98Fz)}w9 z9ty!cTwKuU44jQb|C~}+XKilj1Onf?*nuTq?pk?X#T-4Qo4YJMIP-{uMu%f93_s^W z_zhavpe2b%3x$Do35MkYmNtsbu(EvEmS*A?6eXQz#p8n?{arO?jzh!hE>RuAuG~tj zV>fT;q7)G)!eSNQP1rTdba3D56&&P6^HP+J6Ysx{kjN}>twWr%tu{5i&SmVrioS06JHr(6)hGJ2% zgB9G8DFN`VJLKq5FTI`IM*YJ*0+bb~P)~VZBG>dJW76jLRH&2ttnSQ8&bj@T>s)WA z!~1cX&;_P?QI^T|fWmK<-eVUo@fx+LyYc#0cJ^VEQZL-Bbl&+FEi|a9MBA3o`3+da z*Pa2>x0Y`3R4a>+S5aymJ4^Vf4yd!P(Oiee5hcOC@78q zm#YZID)k0_x&<%eixQ~~wG$h6Hi`|&dHM&h(kOV%n5uc_K(zk11+kOE`bT0Z7xCM? zlCE_pmd$LKDn9UHlQ1C`%@*1 zIh9v_qa-o$u631($Z+PwD@C$RUCc8E=)$#sR3XLMaZ((~FiS8(HN|kHM6sW5QSbqSzf?RMNrhFI zzo1zkXzIKRNMbux#+a*orD}bWh;}G%lzDW7@gd?#8Id2p%w*C;-TZo_1@s!jSc!6$ zt1a2A>5g7Kyi(91yG5vwPUL~S0!X++@+`)%%Sv4>Vyk%*%NiIoY-$avZy7XKI%)!+ zG|=7H7R!RKUI}%KILb|;8d;V@tqgr!+E^xKh=lzd^%z)r937*l4+ofMlj;V$Jty~v zE-Qh`fL(zD9LP{4Y}ed7@9LiM6LbVPcXRJ#1(vjK{u-RW=BirYB~3qoOq#FX*Qena zn%50^fS(CT`#V$>K7H^?r3HvmXrjNg73qtJru%d_kU|v??qB@kEIUaNq0xeH`Fi!Z zEL1s-dCrgH*e>Ojw>?bfV6l8ygNK(ojo$8oFi=CLxqgD-Pqy_KA#uaO^t<`nvzNN> zg2%Ug1r_y?PsJ7w;`MT{KQPE-O)ygDa|LXy<3X%8Oyfj_(KqXhy|^aWWQ)5V#h+Ab5p44 zPlvK6A<&h`)VnMnj0$|P*+_~P zVv~SkyDh@Al+GV`?-~BNdZuI&S|WbcMQ`7v#@Of5A*q-GKCv2cue{joVzVmcKC4LQ z;MIM9 zPvMm{3Y}U?u0VbD?0%{Jx4}b)hm_HH9?%q{d3oC;or#uhGPR!~aidj|T7tIizQYE$ zOjbU;PAe+XpPR1L5#JX+?9Efme$fMx0ND44$>Y%HQLTpnZ+_{=e*kUPbusFpz7LLP z1>IeKPfFgWV)e3%$5LSc;3P;lZM?pCX;|Y}A^St0_y;4RMagQ&;WbIcC!%xzWY9NY zmM7P~6$3pPe&|I^(8Ska!R1;!vhr`GNMN{mh$7-0nhM;A(gFJYt>tnuc*3yIb+(l< zFv}d#4YOsk;pe%^%Em*on-jhu7Ex_p$Z|Pq>6=1oGlbYueiWp3c{$daHx87hDHlLh zQ@CYH0|M&i^Et1pVGG96mN?!hf(!_-Ftvmt&Nmg&7>1AQ7?JUx?EjjjI9xCbU6q2O&0;gPTNo<{~1A8{@dKqDj0V#@LeyjCsYYIT6tNIPDH?8xL zGE%psS}3D&q709?ZsA1N?+RgIy7+N!(n@dJt-u72xj zWjIL~jR#kquXd(j@h+JUmFOOR8P1;%jW?`Uoyp!9PQZxf#}X}k$7%^-@j@O$k|mao zB2byw;L(w})hT7YyUDjDntz04cwKkoz}vV92^m>sq)B&l8EkXIl|K?0{YPZ{T&bEE z{nKR-87F=j>zl3O7IB_o)H9HK9-ZbNxk7bpW4FuE$jz(KBHWd~O)IOrm%OwxC3sPoSXp1IYBQ|e@&jM0VU02PpE}uA2%9Mt zR!T1jj*RCxGmvj5lH#iSN{W1#vz>r)zLD(5R}?-!7s=2(D4Abrt?pOwF=>N!Df$Yf z>S1l<2Izo}=P0Mgg!5`Z$x1ICVk1ILYM(|It#8v)(X3j;Z0Gjbw}4_qF%KV5DP^Ati9y1OS|o$<Y_d?>wM5fW>fH^ThPzl8AY`7yL}1ffGLBQ?86ki_KV#J&nqUkL6F($ z`S#kOEiz^vEkpURa<#NSNOF8B_VtISM3H+SORPPUu>;fcLd~+u(VU7wZHyicB|vK+ z4B_sE6Z0?^=;jqL%qAw#fUPTNufW0_kYT81?rU0t>d%{+N#~!@qWVgqKZQwknkd+% zej`}CjG|Gz;R-*b*}%5-&}0}#39-x0|K~z1&<@5xs+aPBy&S>8iRpg4|CFb;pmy?n@W8}O&clF?fuN8ffa359=J|M>8 zrM#dZT7X92Qo9Ez+gtU%6DQW)9M~#~&{wSd-t=pL$S|kghgNoserJT;J*vD;#hKZz zZj_d`zR|4;wkkG}_M+b)4nPz)y^~T>8aw9)wkQVep@I@S1cP)Xkf)O!8G|{5nL*gD z>hFqG)o=2Z{4*jp%qG(dmZ$BKIn4YGn|)veQcj;vX23M!k-+7lih8~ujJiYtOnx^% zG}DZmh||RCWfGxZJXIF-oqi>oNjjs)ArWU4J81@DCYuxMGgj@3nZh%@NiZTsCC^;vHVjQ(keJ z2auIr9c5sjEr0Da~iidbg14p*1CpS#saEsYNLfndwG6KRzj7-a*Ka<5oA2!pX| zsqg&WB_D6!^qeyKZ~y&p=HKh7O#g(49WrFL@pOfWc~tFMcO8Cs8fZpPfxlZrSmleh zrBin>kmR_y|M9>hK-X_ULYyda-(u%vYRLmc!@ZM9jZSo}tGi;Z6L95;7*5 zKS0z{(i2E1l(z%IpO`{fU3hM5^_u;8ZRFI2s@^`8Fq}!!x6rf~&#Rm(`DFr(^H+Zg zl`uhJdvv}V#Qg?&Lr`LC)i6EV)FN^6XW%X-vvA8grZ%cBfS7$YTcsXDU5I$v*+lIb z8>UVEmcv+-^eNr04Y4UmaUMxW^Lc45F{ry+>}|^FQFbkJoROn8<86t$qxB3rm`eVs zmU@Y|4(n2zrg;&{L9`ar5-9n1C6M@A2?>PWU4aPX7{EXV-}N3%@ltJz!}`0u#YMCC z0BeqBMAIo+#%5E6aYwOxrveIp@q=_^l7&NJ ze1Ix~9ItO>-vhq?u?m=-M{^-L3nHpPKeDzNBx=joWiT zJ~p0*T0UPu_Wt61+0QB+e;;eG;vaokM${t6Ubd6xep{;q;5dcsGoSolOrY$qb(d|L zw2{@3OB zkriW#m-`R!`F6gK2N(V`A1<8|@?RefIsVNL;(yY(ox=5S18+zeb@6D`) zm^~d3l0c=8&{kIxM)UG-L*dB#k{IGZeY4QbXNu4C2&n5ML;HsjOx&|FEc>P2i&~M` z`P;^<_*{oe2FpfMiL*=N8RjnM6yQ?9Y!`vYb!RM>C1$eHpY-Ozk)@U1sP!CvGj-NG ziJn(Cn3^+7;cRxbE@iTZVXeBlbM>j)biCyv0yr5vWu*<0(6TCX=6$e(^Wc7bFcd6n zW1q>fRZ@3#oKzn`zrmO37LGqZ0iWG{Buk>x--}o^uU_r{jMEn^UXRc;uXq-(v>RJf?(;btUN@>;g%_Wf%q zLr2ZJ85nDwX|EdE;YB2|BZ=oj%8QzIkw$3S*@FbLerB}_f)MNHk5`E*43mXZFPbka z>s_|nzyU5N4oJp-Cs_nKc)d_d%-Te38FgqeRwwqd&69KLCd1kK4} z!L-YXx&_WiQ4Q$FL79S2k`;iy!MnXGYgFdT$WOv-afGqqqB^8t4CSq~at)m;TIbhv zh|Arm3A({N;ZfTKX%d-Ei-N&ih_M|CXCquu{Y|2=-ZFg`qjYQFIT1~;uV?-&@MkVx zj$!iS%`X?87R#x*%^}Q#!*$P+!#f&ZNF3weD~Hc`CAwgwqiW@FAo{jiMKxiFMGDThRCgrX!Q zST?Ex9-dvVNZ>X9*s6{>jlhiB6t9j2JFdPn>RPBVbO$s7KL?>Yt-6ML;6GJnZ|pqO z^phs#SJU`K;Y`dhBA#`qq4onhNJP__mmY2v2WHu7#-PixM`M;QXrd=vKIAJ6e|(;+ zur*{w=-MeAHD5lWsb45 zW;^TqBTTZR?b|Du*sUf^NswBj*yhse`J_}<+cH_P7e|J7%#oUO9=5wgc9h}z#pbR$ zsH4F*QANPkj5(FPt**|FZVIc22$wTmnk$*(%*)%LO#gBHMQVZDFTBCIK9`EhKyFKb zdtghW@Q1Qq8Q#27nHE;yPcU)WG1o*T8 zt8$H-Yp^~Pa`rks$qHQ-dy%kE>{(akpVM8TM&hN&ar>w`Gut+N=rw}S0<4*R`e2?r zV^d2N7#Uen8Os$d|3p7lUPorIY^9nwts6e>t5@N8pL5QxqWK;B6ut?eBn4m9Fs145 z8nh4rc;yWp*QHP9E*H9Wuu^($oEVT8u=q*Frb&+SK!aP04dF%-#ei+#D`d&j8CGDk z>esq4{fExk_>+?z{Hq5;$zkTx9p!+{d2r$vE`;|ZdgL$gWXuHGkFRtD#K=1;6poCR zYxn8-EL7rc3@7w&M^dJ5GgS4a~ol1i+N*bo1 zAT6SxGy($B&4`UgX(R?xBBdfBog3ZK-8DMK1`OC37w^yQy1w5(;riwJX|Kg?&v-p| zp68tBdE5tuo7liJM|)u2FPk5~)XOcJv%3b`y~;c^%#;C)z28*WB6iPH_+FM%XYXWJ z)Mb()Ag@qlxol|%RS~UW0xtqxMZyKwZ+*lEE9ui>fUUnTr$*=|8P9FC^_E%-eh;54Q&f=5g0Z%6pYBs+#=@lUCl?`=O0Ysnm zGTG(@fIDcbl@;45^28UNmc^hBdBswEcNBsj4QRfa$oUgLpd&9hV`Y8AF|mKp zLHl;rz{Z5k@ZXaGtoa@<9Nk4~ny>e%h5rvc_$Ax^T;q_%B>C!$iA9qZ{W4V7#0>&$ zPI>#8Y3`-AsI370Ifd=5HCGXd@vm0qbq`6`DmHPc**21%eN4EYOsop570WC7#>Hap3Y4!cs2#4Hx7;=Q zDTS-RmLB^0NZ;shv7xjCz69!*1`e+jbby1j$k3C$r(*Td_`f~#6g&HG@uhmlRv82C z(KKnAP&O`N&Qw77uY{}bdh z(Bsf?noV1~z^i7@H91UkV8A*gUGeX}@Bj(H+reB>bg!S?#K$9o1x37>1a0v3(@ab0 z&kedMU4t__>LjCDqpS0GQ%&~>F8`bZ@GHtqIjxt6!{;+nY@y2_jxyS=mt=Y^J| zVqRf!;LXPp%|N&rH$JB3G~BK}?;#xk+KS%{7LIdYB+z}$3BckR z#t$sBP3@bt(K$sv7eQS$BS_ESn$+UJ*#38fwi=mA_)Arv0uTv5s)A`Y|F0F3WkvEc zWIB*l>>PFdH?c2mVC(+D+*+?Qvq`Nw$?cAMX@a)=r8#WPYS8k-U)y%r@e5JXL}gZ8 zT8cAQ<~w)o8ax;_0MuxianX5SO90AMsLic93@o`%erEV%Q(+L2WE*Cq%Bs=WG)Vk> zdsI;hl1wuYq0KK8GgEcYA=&gO{Xbv79Cv8$ZIodZ&RNQr@~et9>k%Dv*jax+M)8e_ zu&!VeT1r?KG?u*rQ3V*^H4j z-)j#n9UrCSuF}h^y+N=v!wpNUU*|;gdqv-UY2UwjNBmgjdsmc0Kce)5gt0bIcAMbp z`;-7V$U-P(@|e@y4H8^G{adk3Q2o9N1)@xpNveQGRBD^wsF+HT?b2P|NHF3JLf5y0(^>i zk*Z^OF(TQ`QPVoyXozZpx1!ni&i-r1o|4}-a@#hCMI_->yRXEQ?xgzMnaG_FYv-V^ z5h>DjDgk2d8k4C_{{pm|{U|hXBqji)+ZKPEUg$*In!H}dez9E5(;Q)G(f3+wjYk7~i1U*EsoaM9AFF1hp}DUO z$N#wLfvfN*{94~xZi6c~bvX%#LEcs7NlO{5L{+c;`;@8`pt%cWq4M!B28lmpy=k_i zunK;+#1zphi_m=z6I0kL?`HaXxB1gME_^9PcRc^)%s;FGF@36}S?{9I8`H9_-rf*P z_l^2zp5_|f27fbCWWodom$kua`E8}xzH!L^?f{n~atujM=`DZPRd5y!zZklIt=(Wh z`78kX>FvM63dc7;s&dQ!t$Y-%s4gvb%pxJY9(iKy-kz+x&AjE<(Nyl+Djft(&ER!a zgPEeLHLXuRi)noaQAqz*{WsA)`d?0!fJ!6zNLcs$O{ViSrL*Pa{F%Ju-|S+$&xeJF z^a+_H1cyJjiAWvdTib3GU{2wh2!c7R;-IgdTtda$_uXEJI?obeMYa2Kk&PX&*Bo7u z^fU7k#^uwjXWsswMZVFjL(+^;FWaH-1>=9a(!$>>df%zR_UkQ+NC7JoVFW6g+A1u< zL8gPe$H6u>%#p;^Ne)7JSaq#a@(1(#jjULkly>K?CtIik0|MbkA>`N-XqvOlp3^spE^hT#|Vh4CV; z`qw#NOaS&w?d?47&+zXj$+61dDj3tn+S2}k_5daSkB6)bL03;%u!t*re*6Bmc!1G@ zmM-&5Y}7tN|4+Sk?xF)v8e@*>xCCKN&^@Uaw7mI`uks7}n|KLxzf>-Mch2@VJiRX{ zEi4w${+!S`hnPL}yqgo?)$J^_aBUa8qFd_X&O)LXtItzo`ccubSNnNy?~61OQ_gIH zxCBB1*agqwVI8FWiP2WJr*<00XqiG#+mrVxbxwbe`^-H)n6jn3&(i0-7_Te)7>?n3 z^$9)=mYsnaIu+Q4Z+HG;VRb~l*r(_mIQVlt@ig8s`n1Q%^Kz|Pan_*z?fcB3R|Kb+ zCtrd>bfnlw#tmysJ~Jq-?z?AZm*4o4?Tf&?J<}w$gY7X!=i0$<&Z1?VT*kAX$h@+H z1looFGh9xb6fVeY63F*1pWu1W2G^om0z42ztL2K@L^?^f9ji5fQ7?SI-G-b=#9!~e zc=S>QrHjGNm|XdP{WK>aj2+EIL@Xk^N^2sR2-420LcpP=!&^SJr()QX#)gBz4{ zqG!wL@{z~KpI$o;4{wjVjUsB1838ZDasB~hjsnB=!-uv&xx(7`DOPaEDT&WxL)+qK zlw1Hfm+(RRh~Hffs*_gC)v^c2!Pm|OhxPid<@?t}-x5dDWzHc2HMMz;WV+Nesse&C z?Ic+EmF(3o)X>6?HQg#yD z=DAADyeUl~40-?Ii>a^_P1au_LFaesO4(={+DxvR9V*{>;e4sO3%;0xvwuMXd4& z8&TE1%`T)wq{ew1p8hHy5#V)P4^9oatrBe2eq;fpqu?d1YNXXE6>W|l z?n$#my2j+qS^3|8P_zBVZG-x<2NA_2tpD&v8hXLThs6I^WQYZ9{LpPDyhpeh_TjZ4 zp)6&Hy-E|6BP%?z?-DsbaqbP$>+!nby$Qnf`*SXv39L%@`)gh;20hCg0rdACqY;gb z4V-}6)A;}LYqK=}Uxv<6?ZdqX(*HXb_&wv+-GpU00n?TLODa12m%w)Tt-vV#-?RVs z-v2)@uPmhbT&$lN{(56E@b&B07W~5cu45}#OFf_Ef`}Mm;q72utL+=AAUyqH+X^4l z-q}y=OOMl+h*Yj6R>$=m-qF!fx{&>A=jRp-xuhewlLN4#svS4|{$lf6I)%OBOBnsFprdAf z-b4fdsJ~?}ku<8exy`pMueib-O%!qT@f-S(s}zpl(=p%8WQ}?2XbwjFunN|>`Bo%X zR$R0vv>z;A-n8CLaK<)kl3C>dvg_!C;w+D}uynzC7=GRun|y&UK3 zu>evr3r73hk%@>jgAGCr-+k(A}k zjmy*SL~pGZC|}?9uU;T8NRmytKjlKs;>y&uk*{x%BBezc$hkF>J}OscnPV);#o6;@ zH*fRUy3x}opd-Im8MEl+yA0a4BV2K8*izVzhL>!EyiG8E--3crV@-LzXdGXjvRi*8 zKOGdAKYr!<=^`^NmCW23Sny=3ni0IHmJ4eM?t^;*M+bKhA z-f|=16LKTo4jTY`OE@hMS9#dDo%?VZJpy!dgI#|7S@Qe0Fq_1Oh&8gDlC;fzOuU)j zb+g>Fte-YyCriEy|8DPfg>xSsd+c^A9rU|UJn5o8Tn(qCq3+XFq-$ClW&2^d#Kn5C z;SjV~e@oO778VZU0#<+gSEUksWt#^)&8MB~iD#>prZVst?yV?9?A{_6+{Cyzw0t{P=7yT=0%;@4rfJva(X%&wyqR)#qxq`h6Vb=1GCX<%Y&Mxoi}@> z6*Rj68QWvKxIGZE6dR~i&N<_V)0&<9W$1E&Tw5g7zzqxxIA_~(D!b8j@UU#{@`DsP ztp5X#QPA@<4>0GrkLUlP=1`cVQ9?d7t^OGAb1*B>;)V2sjbejcSv4)1$AdF#rnnBY zNap9O{JRUTPj|*T3H)cQ9C$CRo7y-Vj$zoSsQy>?4$r1d-JT3F<|z8nboSY9fUb5+ z)GlrX&dgC&A&m<=dBNv%-wBWFSHREM0#3&?I;B{FPrvMjtjS{JP(8SI!&~^#Q!=TM zTgh{od9yEJh{TiD+pi3TR9LijP;MduwleJxqS#!;QGbIjJ@;4O@LIlJD8Ii{ob9TKdwRaQIMJ&cYO?@1v~<< zUHTulVmkUrKx2P>cE|bpCMH6*;$@e`hfE!!Zz(-Ak}tOOHqQ1(%#wT0W-Q(MXj7O# zh0ZkmlhjpNavnbO4tF-!UJ++-uPE~HXSXU}2B@EbgVoE!Ywm-${iixmGwL8N3%c)I6TuFbU|!IG5} zE?ZgKJ~{xn4_m02;@lm>kS}Hq>tmdg_`qwKqu_Jb#Mr&IjT7av(ysyrF|e zeltltf`nZKf0DQWh2szeFOi!?Vd&pwW9Kfm~DvP|y7gh1&2IAj}>7PHm z>emO~S8!3Vhf+9#mK`7&M!Rn`qov8wV-+Iy(VPJeecQN8bmz;E0Djudyeeyjv!HgKz{~zg zKNvd4{TR5%ZMf&%Sy0p8Mx@~LTkrk=nOl5n7?)4+u#kwrJb@?O%SiVT`l|U`Q(!pW z6b>hN;&baf6cLB4z30$;d5jta(2n3<;cxKAH#FYkG2xJ^Z20w|TUHHy;>$n-`}>@Z z8kX4k86`n8eTCh^$=hXk6B+0eHWwHVQ{^nQv)|P;&Us^G#M({D8b4blz4z2mgTvi@kSK*yr1>H1lfc%oqsf99VF}C z7ZK318b%o=B<{yYYxfXn=3&99lUgIP8GP`!H!Ar~N@o}J1B1Hk?LPW%;QoTjF>SF- zc6}tWk`iBNkMnI0bJ0z9sF79~V!qY3N(cp^vsS3Ax{%D|JnZ55bN(5b|5FI_D7x1E55t0GW?e1P=^p zIzL{Z$r2NTHhK7UrT+Z+Il0NoO}z5~92&(mF!4jm(~57%l37PuZs#3GXeU(&6*7D{ zG(`l!yTz5K!F?l_KEaW{b{@wfRh2`1B1C!V_g%nc-My%KjaFk$+`xhO{^FrEgjL3PM( zHmg^NOm3kidY`p5!iETxbW9s~fo;i+M3(HUKI#@iJT-lc^t`weug2RHv?cN-&OaCV zNOV4aghP%?KkGas?796-<4CaBg35O<`Uf1`+=mInU;B-hb+b#N9m(6GWnHqbItw>* zXk7?3KtlA!>BcvQZV<}jTXY_R?a{#_;JpKS2F(J(42R8fJ31-qQ^Bdi}PDZAMIa94l6aTwG_dzkW; zl9h%mOX^a9)|<|o9e*E`xf)3grJ)tAcx^ROPv9ywtefcH2u30kA_GTg0bG*nnLvw| z>dbwVx{{e>>uY@?If6BsWTMNJ&FCiw`JfnyajP{QYEbdo?X<#nzfIYO8u7=QeWv9io7=3X4IUdES}ZfR$|~Qa z)$oPW6aWiW3t^o%86koq^j%cd9=W;jQ39q_Rr_@ShqtS5XrBJ30uX5X4mC|YO>Rq} z4&zq<*C!rI1>GTf9hD{?m+`MIDqYsErwQ*s_@8Ee@2=@0c0BTYsz7)g*P@j1``z^# zqGagiHhiky;=~b>lxVnS42w^!Dsi%2J&r%<(^u*JOoj-p>x`&VY>Ou$2w_X`HtQ)w ze1k`O*hMB}O^a7c><$vkLbkQqpHkl35{6jYa(xeQpR>F*A+~Ds__F>;CJW|yDMY8i zj2u+W|0Ql!pM%%@D`Y$`aEB!__%^VjJuduP60$~ekBn(%}rjpW_9W|$&DmsV0C+oQ4>w(sE| zC9^M<-Iwz0A&$%g)ps|(IhFS(2)+{v-74=_O#Ip!U_#}V8QA8kWlvVQMvh?$x7Y29 zq#eDL-0-+a+Ydc$iD;?xCVy=AV$gT=ht!A2yCGDSP4ZDwoJxjb&MOY94#lo2+`*7v z5?sps&C1%l?tLl4p_devmhr%1e!o8NHB19vGDkIna{(PJjt1&O`WC>)23^yK zCPFv@oe{2+_vbrjQO+&qojerZhf{(AwnrS2F7N8reXMm+_?Ty?xs|BYRA8}NnKHaB z@)exRe@tzqoio9?xY?T~6QL>mo*&A0*4skoqswM>N~Z6yOW?rv$LM!zRmbOV2K047 z>HLbc>AE&X-9ev4-W1!y{PqGFDtB^NUnUV68;M^G5av!Xt7TL$Bc~+)$lcqqLF!Tn zc6?$uBH<6UpjPp@#hyjG8$l{n)qLkuYbsJJx<)B;$2T2~@ZX4OP9+6tr%l=ite5ihL#kzMh0S1)W5wZqAgV-x0<@+LGs7?Eg6Q@JVKfrgYrs5+xxga z%#|Md1GKJ!el#{CXI2BP3eo90-k0g#<2L%T8olU#XP~l*9B7p5u--)B=1};A6zK3y z`Qa-n$@FPm_d2l#j8at?(N$vbfsN3=vWURVf4?7PE6lD6&_5Hb9`X|Mv1(6rHxl_9 z7Q+B2)M&aBLnn4f_jUEB+xVCatB_+{dm_X0y}~fy$>gudi_yPY6dJ+U7?ujIP~Xo) z9HqBS6;e!d^Q`;N;ePA!7V(8Yq;ZL#Ql9VNQNqTE8}&MLsBG#FGIk0JtQj6IqbuAMURr9X9J4Q;>o&3HAXe#s9!-f9m=~BR2tRK z4WAjJlq6E4x#}D+HqxP_x?r>L56?r5)N(KTBF65XDCyNzHhBQ~{W95YcsE$Nypiq1 z$u-em^-1Q(3>2DVg7u8xnn+dP?BvG;G&?kqup(@ zf#d4sg}t`i^m}m$KLJB;`aNF*#VDnN*R5d#q9s77Puv{SeVx#I_ar|&f0otu&~m)E zHJS75_@k80`m%-g1e6F6$wzP};#&$H)xYjtL2R+`a0Y~;6U+IyZ$|h6gx4OuQOZgd?3d43HC%=lje@#nz`-CM*uQNBXy!ZEzezFAZrZPLsv#{ z2OTM}tHchzh1c?R=4mQ)FFog(@yV#I#$}zk>8TT6waFUtP6d%;cC@p)I zG6Imjt&W%JwfFL%EL!>M{|_;-QQZ_lTk_5#gR(1fTo2`=qIsKM6;J0tP1U%mZsSM! zj&DcL@a^B2il1Ab9J-LOJ`q*_o@P~yhgnxY4IjtfgEjrW46Y> z=6~`Y;|E{#w^i8PKBdX%@&Y>K&|;3@d>~ZKXH&u4iKM9Q&fUhXEeckC=1UcVr#
KVFL%=BUXPz-335td0B|+w9Tq38jVH4)aQHf0u6;w+!Ol zcqM~VR;kfcNZglnSJ3YTb?+ze{5RKn=FUEPxJWcpe4@rh2q`p3Q+W}}c10Nv)ZX*= zwct!F#mZZ5r?gxtJM!-?=1He;DLm82h(>4Y3|^;za|K3dJ^Exz9kNwGXMyV{9*lw4 ztN!OLPdlX`!_YvC9}qx)k`i>y31Js`dyCXYQ89N6UV>Qh2hvD)V2S1x2y~)d+^G!d zm+SYx2xQ-BQOVXIAHv=CeB-2H3-wH_Z)Ib%IM12|pcrynoOMi)R{7*T5*icycJ?5) z)%8Llz4iUY@01F)x!%I6D#=Q+oeS0E3V^(0Oe;eENo<9`yk1fT=jHDtfX*CoJiwIW z`^BBFh6NX_%#icP?SID}V7HKbyn(fqen6+n%de@y+M_?_WOH?Y^|4tR>`9#+6n{L> z?tp#^dN#1k7t}al40R^_S8T<8m2jO!4Vz8LD}o>&QlI16%{sOmk#^A<=_D2f%C}Ca zQ^y?oxv3E`U03>W&&SkE$x2$vKul$;n1`Su7M%p&g#IZE4f;$AsjK}6;ZQJi8A~F8 zbJJb>YYa$n+(p@qKw!us9&YM^-kFxk92##5r`co)>`EoQ5calA6c>xQuCYU<`s2D~3Mq=zR;{K>Mi>D}(GH}X$APMHJ z<9K}T9yW1LgFRFjdViK!!DW^qcWxs>+5Z#-nyn{g4Sk`N1&vJkdHM^a1@IogJhrxE zb*OHiSZ2@yJKnUWGOALJ0l*KE5FK#yq)|22jIu`>>e^OXV=-nZU?epap> zx(ly7udbl9pV3*_OL6(2ukh5)X*TPHiA&$ijs3A6*kusaRen6Nxbu7QM* zR6_Mb+xIu02_a!OZ2HPpeWNJiu0Eq|-DYigC`% z7isz5Y7TYZEWb``4@_>XCwLv^(_)Lp33t5R-|bh zCU6&fT^Jr|(5l7|w_PNrd$;2e{Pw21B&F3;>`j#2ggCIvU!fmhrM zZx5~}Gl3#|GDeg`MT$GRUZkcEhT*B8qi#fa6vO4p#}g_31uGN4`F z49+mvLvHTeq zN;NKZD}sAdvrQoT;e$eRsPv5Q_pFt9zn#=~jj!UuDtSp61qAZ(8f~<@OG@3sn%@BB!pjwl zlKQD%=JwsrZhQ-S3$WVi?uy87EsV4lXDqk{DfeCb9zWIBxt^d!|ASEoc6tUW$VF&Mvcs*uc8FKjNODGtXu5bYiOZXM+2y zPaaCGwUz8Id#`{R`r4+Vx1jf8F*V6vPx-+N zU*CtWEEFg^`q7!M(9HDmbb|EZtMdopUSqYo_qI8PFHcS7B`ju&yQW_3#^k%rY}lL5 zp9h6&%AYmY;Y`^BM9>!tn%_R`lv4{Vcp-AW@1S57(59NTg-cY{qwMHBZ1tL2Yieut z3BoUC_uM($V5|&~7u_jE%7B+uaBa?$>i(4$(x8nkKR|%QeuXL05 z7kuv`qJaK(HEAnxk^=3`WOdrhEmwZWe6!AixGYgA>){du17^x z=84o@>FR{R23O~cnCaeL$2|bhE^-f9!kA9mdF(bA~5*W2Z?4PP%Gam^GgrXO|h>y=774$}Lv*N-Jo zxI$Ir@qW#9b%Oq+3+S>H$c7s(9z^>X=j?c4EoEjpZ!|N30dNuNTW4FUP{ZyK6KAO+ zf7o?s_Q}vDfBpMAe0*7{q3o~A6q(yl(GdUl<;oWZXcW~NXd@9#=dKlwx~du!W)#tH zksK9Hw@{a`@bI|y69J|!Js8^vD7+oth7xe(dVAj=hdsG{_&`vfl<@YM*oA@mZ1>%Q zLu!LOz&^%EmUf=JUn>nh*|28gjs#A(< zJ(qn~re0;D&3sqM-^%dF-+sUPnmlo#D$Ksw|8lvmd$s`XMnhVe5Y%NaznqDoX@v-!~tZvD2#*`mzkJsTsFTKLmlu0Hd)} zqJ*cpW?I^yPM@>bzosxpaLA9j)IKJk4wQCa^hJunl|Gjd+C+#bn*}#ICmiyzB&zMb5 z4$5#N1yt?U(p^^HM38vnf=4mnKs^9Kz@Nz1=trq#Jr9m>lpI#j6S6->_Oo+BUg zUB~`A`#E#p*xP=mHk7O7$~_#!#z(%E;q=#=aiuNZJq;Q)xN!x#l4Z%m<9=DM3dNyI zqV_NobkB-kmQ`=PzxO63n>bSdvjD$dIA-NvE!QJ|C}A(C-(K70?kKe0Lh+^T z8~J9xiQ!S1s*;ZSi$DB_v&);4BIu7fb$$?&6464n%vW?`T3_x51xZjX6mD*K zV-goYLB}k71+ZSGk&Bb(zkTaHKcE(_HzbeRS3i&oxj7As>Cu1fvO$wnpHai#w%@u**H)-1ah8|m)Vr=+ z`KOez!48AJC&h2ck+*o$gpRF5Df%F1BYj&kwIFe?P6H_^*C`zNEB-BoP_Hm+&N~s} z|FtRI)=C}%*of|J?OsWJ4t>!6)ST)fwO3}pfsc2ynqP?sY1pc@x}EdgblqtASZM^r zRBHDX6p>;V=&D=G2Lgcw24(8cFhV#(tq&*@m_w%3-HnogE9)=$PxkiQf5GKL8Zk>| zor`XVMcLa+4GV6xQ>Ea;!k61B-SUV!;MMoU@-k=6gHzpkQk$Oc5l5a#WsE8Kuq~_Q z2<=Lya<(-E4=i!KdzLz*WY=D8zW3MLr_@-Xg@H7a@23M|e%`&p^6FrIm7qWgW>{I$ zAuNCuTBTv%^9F4nuDAlioJ2~4y%*1o+x}K9kdO|(Y9;e4)iAv(U74Tve_^_JmZe+8 zv~d#2R5KOzH}BJiM{Wt$jh6J?TFL$hH>u28@#r6kdW$PGRLK-J0z_j6bG27e-*Mu| zo1UCLkm~;HLNkzp{qafQFm^sT`}_j0Epln-j}&OP4u8ve`K#faSul`{Sw5zM4rh5P3csd;qZQ#&J?W@o+bnJlIhi8Z*naHZ>^RtHfhsoM*I=n zylxzsL12uI&?B5P8!Rm`bEav$#MS05J>QZdYkJRIM!06M4RF)tTiXm@*+YuLY310X zPh@YlR)S>T?>D}R;bD&OtP4sUbh%F5LSa1uEs1GWJl}*hGDt6rS#$P^kRKP0u0%S> zFmuho(?D1FbL-Vz1mB9vw;;aHhl8`rS_okl5=TS+OVMsyA$~<2SCQr9moz@w#iXd#D@@Aew`Jt*mfe z$9V+qsHl&C$p5!b!P#5jP@|P-@!u0?ApMiznlp-to2OOnT7J6A2LcwryhpO$(5#bK=TzI{8YNFx$fJ(`F)%1T1^Z(@!9hX@6_PJ_YTxSuuX*Ne|Btn3&n;^}RI>|i9`2}Vp z2=Y2OaS%6z?WG2mdp8@mZ(cqwXl6)%Ql>_>+z#C3cswmn?3CMEqNCGb?W}FW7j*Qs zMmaCS@*uBluQn6VQ>&Di=O!0!gJCOwRyRGfB2=RSf}Xz`OkKFD_p!giztRg-EBIyW z*`n5I(*k;(en=keXrDWHJuzbh&V7S9&l+m1<9P~hnykk_RowC_?jiz}#RwnCh6TNC zgQZ;>a7ps?@Pope{lVAYst0Y z{^ZH4yed~eto7PibPb66SXwMR^@$t9pN^|n#ixFLUK=Bmm0;y7p^a&#vrspZ$hPL1 z61V0AHJJItDY_hLh2coi6~lDWdxn^M`cNn8L{BoNE**zmSIbPnr~f`{Jr8yVN6~ph(r#$3xMX?*!jm?b~u7LvE`PkMC zBTO$Msbl8}+cJ+aU)mY=D4-Fpxppv|zLZ}x-ymQ-93&7CFWoo$koi3I!`-tt3(Z9< z;|a}nYB51pCZEta_i1CF*RL`4h5$LU)c38XuHm~d)!u864+k`M8>;;x3=5WT!PO$% zIrYG}e$+ZbW3XHujN9uSjOK#K`n#oPS-i)fI%(7OEm}>MTblOR&l1yRf%uIk-{$Pd z>V^GHxbApnXz0)S2TKX^|rC2#KDtw&&GhWeotR=nTuwa5MXm< z?8Rv9X+n{E<@(F3-&UPn?DnPhrGJeu?_Cx}*9PEOvIu1{d!~_erwWEYWp;k`E!U_2 zP_35NWe6EQ2zs{^p$JEvpsuD8(--!dfd|k9eTY&o5k|cbvGs2s@uOyc;izJn=4k8_ z)AF#G#pAs5+{wW~w3YgCH3GlRE|GqHt!r6=czc%TTAp5QPWm^1y?sG*Ke$ulx=hTw z^=HCNYl1r$=ZIoCX78M)sIz^^GZ3QeefQpAd4T@ZXtQd1HTttrtE}-^L(?RYakLF$ zy?uXQc4;HrA4!J@zo&ZAqkAf!ZF!12SaQNyp;W-jhx#^D`(vKu$Ho;EVYLCnV(D1} zoK9#N72ts!QL}{&v+I~Z#&I}6>}Sq*AcQl)Z6lE@GRq%UHT90`hYILblW-e5qcE3_ zAChtF)5aSPCw|l`8uiAAV1KHYd{@SXX~x?W)!tM0&)G8eTuAz;KM~ukH1Dyzk7cyo z&(+Zb1r-MQ7+EaMTW=P5dG_&*eIpJsaXl*o8NYi@QPZFxRTYInoeQU-!hhV2sz>Y+ z;dh{wfDz!8)#b7qZW@IOKNd4~D_Bd!J6u9q&JGYL zoOiYN{R_{WPqySoEwa+8*3vexC7mU$Z}j7?7Vrf6?hWyk@r8ojej|CgX%t+kiLjWT*gBnnKq5KFL-rVk*`V186!)2O1Af0wNFa9e`TXWHD zDK0EDIhI(lwx*>K_;^OBHP)U>Y$PwZCg@mkh5CE~wN(tgO(xYMet?64gg5bn6P}yNP3`e&jnv{T-8~W+D zPL2_v0-R-l9YY-ia*0A-EiY^X(o6S(uM4<7c7g3KPfCFA!KiBpM^0i+CVm6)cGn{N z@635O8Zk$9D`dC{ys^E!D1f3pQReD)AiU&S;$NXwb!GSs_9mzijP6IZuYHgY#5>RB zNszL{IGp0bnZhd;Bi&i4Zg80NrGE>E0?W%~)Sm7llcm9_TZN<`g4S-0H)ErFpDNLh zh@A#Hm`N7LHzK!TQS6#zR?Zxo~5&&_VmO;1fxr;CI8nZ+sf$75|X#CPFSY z_Wlf&&h&RC8d|u8qn+ikM-}=o$a&(;vA^*9yDmJIl#)3!|x@i$eTZQGiP>X;O3rJN9MfH1GVv}?(+~83sRLO5D@SG{# z;?HRC8MX@uVklTF(Fp?MOSz&~><1xZ*c1pBh#k4DoHWLFB2u_o&GE>a>*&E1yc@n5 z!G&MCc}58-E{CK2@L0FRs_lEP1dV5m2P=ZMI}z}f-ELMPy$*LKHPoIP8s=srEpvI` ztwVRe&Gzy^`v`XNdegJzB)CJLA~+-N2BEZKPjL-WP3C8PsI;tPnINGGGvc((9eLqW z;&6;!KQde%ECE4kd%ajyK0hSCsNM#3V&CR-xQ2yko8-Xl@vRoeHG~)bP}XlD>gz4$|~s7XNkumzQ*QtD2^~IQ%>(&Ft=7HtAE*E zG!|b306J+&AWh`wNZV9v@2Y?}(9K$G3Dfh9(8Gv0Z@#9fNJL8uqEC%FdYpc^?dQsp z>}&F$dv_uBYdhfjmOnZAZXNyAO7p>-o7bgRY#bs(oeEKje?~py$$lKRT^_V30HA}y zt<7=jpw4Zfpn}xF*tnx;z}H=OR+yg9_v$?fRxQL3`3WevC@9Zpdz9+dPB|U$nv9{nD1Z~dpO#L^Ts7X(;I|;rc?u|Yw7pT(TG+o==0)PROjiR z3g#iy8*p)9XMJl?GUQgF!hW?3#eC~!XTh|4FEJ6 z{x-QCYrbngA~ntgfUd~h2nvY7f_>kqw7|?%bbav?ked~A@CE#)77@<$f@n`fm^3>J z7K8~@e-DlxYUIpy18KOY1`6&w9R<6AmYO+He}J4|H{j+}loHX9eO9d$#s6UeOzEUA zq1p$T29NQUH&&Z|9Mr)ueE&req+=f2^x-p|FGqWU-|*7`xsgW`HgU4|Hs&5a z)9YI~c&#F_WXe!{cv)IK2lt! zXgt6!C5aL*+cZ7smxx;KejeDtVDnZxA|X^#?%KTE_t*a36M}RV19ZfWZ>pNRkik_e z3YGkMp8ifIQn#m{4*zGRhw@pBF8#m*-$QW51N2XF-Zh1LF(^b^(ZzkzxGZ-|D8{9d zlFK?YShA}0pLu?yIaTT4Rjfn+pWl;+%_jOQ)CWr1O+xyx>iHzVBx!}U#|JYA4NHk_ z#cgreOQiWFHr2L!Ag}G8Y<_gCm2N4={o+!?ccD$>41G4<`qY5qeKcFJAP)JAlbM8D z$4ZXNpctH=;6b`~Bf;Y_Bh_=^&Sdt)pX(L=@O8PvXx=Z4@YdlLcO46-@t~AD@wl$d;W=Y5R|T@L*%|((H)_aT$g(u+eg^mj(T?(S%iuyK z>1qB|`( z92|A@U?lvSKX)ll~oa% zId5lq{6F~Dua{RJzsfNUXVozhj$el}5U$&wx=NUiY^w8f88+ik|AvrT*~v!6+&q*) zOHH}L1*VP(PRBQiDQEJ zEQz{_>0d`_>W6oa;3JYLLY}}OujE-u;26RP-+7(B>idrHmNRuQLUECzKa1cK-A($h z5?)IAS>@yBgx!2V3=`8}Y5)xly63%|QW~krQC<+l+Y`s$N62HupchYhZf7ZC41#pL zDB+Z|n1~TLkD)Pfw54B_4YUcKL6j*NARBDo%bKf+>A5bof#S>a3y&1bFLt zd3U5*AF6?u#yCXPa2A}DF<^s>5*`|93H;^4Jw($yP^63p${~BW%G%Wq174rX#K6qt z@X#)W)idpaaaTVWG4idI68aNv%F7q2HVoE(e_!~SaIGDXotNUf@;JY4F(5yE{#n~AYk8{<6CG%C^WLn98z0gHCj zI`!5r-ZYnVTOEuSWp$SYx{DSD!|24c_8`a`DFAcVsAyVHn|@|cr{481y6)mnsJzGM zhF$du@9*)O(Qib#-PfNg+^4veQ41WL1vf`4i&?t9)JApPsH%U;?BXRndfvUhlv~e7 zFY0p~n4Ify^_FlC7v*rJA?Dj0kn3$DoSVW>XEkzVq}T}SWl@6Di})2y@WH?auHme{ z(ag2tW@Uu#{Tzt4=ZIAj=3F28@$iwh!W2?IOP``W^VkKE(db>1^SnQ!Y&@4~65V{Q z>j8?dRrY5Vnr;SP5rw>7sB8!yH=ZTrM?{Rsl&6l|Pp49nbtwInccN=L8$Z6gAcwEL_&6kb#K<%@Ng`(l$ZTM2vCnu{c+TlszMAB9|MG*wle3J1fCDr<~V zi1#xj1cSf=gn9)E-!5^ey7OY<2cJAUo@CMb>BqZa`_xIuVIhWUDEcaHy=tQRJi)k5 z+1=EZveedPMlaT0UZt!FthQ%0OZAKwSuMg88+QoB5O&>l2^G0L71KWk*RlSOqQ&gp z!|@7#eTeDqQ;5bqw|RU9x%zr2 zQgmzB{*z1NKjZd0r;8sSaoC;~x?*75YAkPOxMruMHh9_l#H0U9M7U6)2Y z!>%P Yz!m>L1yknT?@Q zFf}sVH~ct82P3aq4oAhY0 zfr!Ttd_l=Zf{XFG61F-$HoCigyc)i6nV+0ePgku+X@0;|BYLl-BoLoJn)`?#X zRyCMI3oQF>37GT9+AG)hS#^1Oo&w@Yybg~-MQ7`HG$zDCR9X*I7GtV+gkp%S{pt&{ ziH{dUJv*)|EXI50L8FbKJ!J!{_06Q%WkUJA;xx|_Fso~H+Y-f7ish|51W*1RTx+)# zBrlsFeAegE2&AwKCBpkyhpFYSe@F`+lwA}x%O;k%I6lujtSEu@wZMlTBDy1IpF z4rM%qhjaK2hKZ{N7knBS3CkV0pXCjTN!LtzFZfC(gYu7}Ol|f|EMCn0-YT#92hu|V za+>f{v>!z*1cM{GRHprEZ_YKCaRUVyeAUZHln{Purv~cj(C0qKV5k%PQg!ka4;fv; z=S;R%>QIPajGMN2=VIVhIJAl8H64%{Buu4WDJ_5XMXtN6Jnbf?_hg{-p;J5qBRIY3fWe1; zk-vRLey-+NrdaigcGpOyDdiaN7aVe|z>kPnceaie~AmcZA=c-R| zkeuPc8G*m4?iLRSS|7^@=yhR zLS$bu31E=l@xhNQ(6eVdSQ0a^U!$EPWk?{6aUUdpp%o4-|)M#S7j@sir&5$)=x1IAN#bqc2g<1)v;rj9yT8__yzH4IORD4H`o+V_ z$k1?wdUhtYD6ry}6#W77)YrXL3e|Or`NfM3U$0}J>xM;^?qO%f0@G-h*E{eDJMpR+ zk|r=`iEMRJ;RKa;19uc)+3G2X>uEHZxem$${>t;L(UZA?+q^8XRIzGaTMJRUPjOY< zp?(X!@U)pk%V*b?)-b~^e4D2@?OrDEP0rVCG(i+zEga(yVYsvAgVINZ|0omQUzA16 z{^Kc1CkrS2JR_sv7t2M>=X4Tjc-=!?{>9YQqH|B2cmODV!3g1(BSK-B-#PCC2Ql(mwsI4;i6%EjEa9FpqSh@% zJ?nQ<;q@&gS3O|BBb4ui>&TBbV~OMbBHmOFzJy~MM%aTkA5h!ZwZZ@GlJ zbjP9tEQZf43`kwCa+NW?6Wfnx@jP^#7@Mjmdrb$FP-{I46N^Q`Xnt>W9&Ee#5B?Zs zhWm;WR$tDiB1GzGcz|(#C)5)RMhnWb9_-*MCVn#jr&MP}cwAZy6-z^_izx))zMMM=wIW(bk23J-p)h`O4U;^LL#A@kx_@X?* ziBQeT>pWEov&u0N=BkikdW+gCOG(2)e=~A3e4l;-Msm+WTEDt2p*DE*CPeweHTuAh z^)(za3XB+9XwI4M8nbh3zg6rFjxtAWJ%g`opcGbPEP4^@Gg(rFLQ!1#Y z4MNVzpUM;NX){H*V^RBUg8u?d;!S%;X3e-3Vj^qvFK#G7;Y@v_aKTh|ieeObG-|aJ z9g@V20=^Ho`irS%&MMq7ytfh12*^lHc;s|fU*kpIJ;QEQl@X~`UAyS3%`s%*@sP5G z7r!U45}k~k^!sf%Jhh;}sEl~;ng`&_%_R7YLRLepZ+PPxkLVR%%|Rib6VAjhoVlT8 zS%QTz#}`(g)qMTJ-NQKL3QoAtzu>rSqyoQhu0JiK;88g$Ll1oP!AB|_m~Mti`dQs& zsosEQ@#ZQ`L-RqGE_v2J4_`W|4=#NI&%-see7Jw;?^;(XcyOT~5A_DmyGt7bOKrb{ z<=xY-bhFj|l?welnFmGvxsA%ZJU$ywC_xi=R?zG-0Ei{_%W4y>yFpZf7t*JE4GGw! zo+Ek;nBeh-2m$YDuJX<>jd_T(O;8dk8=S;qDRm5dER16AU?0;=_@lNidWKOk#d*Ii zgFF|Gz}T4pI44M!h8|D#AtA4|6x-Y4hcO(BUl{{YLNUBTVV8E)CyKWWycNvGQ~cOu zH195KoX3b(>Aa}G%SJ%@q+2#HF%)9^`xFOnQoqYS^~!`Dh?#i!s31xyMPM}3qWyvA zS+^JAb;3XLlAjoj_XBEup5>Tn%A%SXN7Ls#_S8cYXSW$04j98nK$N1`5B1R!x%-UC zb%J4uE1`mbdn1gNLK6OU)dl01Qg~1t%RY~)<#Ox`1XIaIPUnT5j)rs9Ev`x-9GYYn z4mmzBx%Lg%8KJWT%n*N4}2YhZM07H-QTvS!p6b)mr>)OR6$BCp3AVr&9!fy<a!9nV~F&uyh&i>?SlenFE7V%mqBI_&f60);mbayzdfEbUmP3H-(3&+=lA1um1*bk2c&-YEO^&YhBW1Lru67RrZyf~Ve0YM zytMXSuNgw70IVv2FT^+6zKF<_GX+i^u*!%ev~m>@D1QdxMM`>t9q`ddj89nLJdX8S zs26iHjwZ=?DaNnb`$;u_nz4CpVy76w>di6=#CDw|hHNm}@1iT7p@dDXpWSc|yhrP!#jpq%zapxUC!F-AJ1!>>_ac%j1N>^-@?uqJW9rt z;Z<-8FYPl9LTXmSfagvO)pA9KxcbHzO}N7dZ{>AikBFOMrEpU#84UpTVZ`tLuK2}kUE=fW5db}8u)&kDx)b*42!un5 z_t=i-o&u)10b!0+*@1UpN_NI6VuS+Dy7`$aC?Bag>-mHoYkaVP_kdG=I4H6+?OrpI zl`#h}Mkx?EO_zO68HuZ&-59}EcGvzE4!{-fI$1kpaD<3vk4bnB)WL_1h(-&?3?P5K zRrujP99NbDLhaVk?iu#rjXpCs&*+UKf;YlHeoTRa zw}N3g+6K?C=i1G5N~87g^sGcnnXiP>j534YvgYUQI%f(a`%19ddUrj#eL&*Cr>)Pf z>HMRgo=NlB`0(ML!3e*2XP84uTezOR2(%lo}BqD@q~H1Xtm zVRIGW&%&_hv$3PzQ$o%6r%q$!6p3AhyG8~4KCwp3v4vjHcf5ZB6H;T*VG6Onz2Usy zF$1q+CeNy$V!MqYZ7;W(3DWNxiluADGdd*1rjkyzV=3r$={y-j{e0~4lJ4>fPDTm# z*dxGL=-4=KXP>b04tY*uqKIS?^{jTqA*~)U=)DA2k0-7i3^(x61=ib`(a`qJvd+G3 zauPbSq_U+1^}!nY6huPDlV(^<5c=lL9oyQz##R#qN~HAJWD;XBCU|I;K5Ed9EX&=s#WUq*dslV~AXDOHzmF0>v_Yw^5ZGdAe&YLj2XxHvVJ7bR_!GtKt zZ}4Kg!iYA_8g3dXph-sdB3?Ti{#e5;oF|j-6Y@;1BEgIrj8mvXisLGnxD$AXu^3+M zFZ%mk_v(9GpK;aZj3j$Dqrq`>GH&D+?Da<>lf;?HXjXyKx^w-M2hZ6uiEm_qij4r_((a-u>9tece3)Y*&THG~xo#&1_>PW9{y21ae)ysKsz>N-mUY(t@Kn!=2~2`NiVKLFu_VT*}V%PzQp9mzj_Xm+$I*$n{mf9$NXT#eV+$ zJcGL^cbC9Wc)aj1KT0T0if?#}2}oRK`5DB_eTYYNjA`T4eW93b^6eBcZz|K@0D**r zH-g@_m6TXOa_jio!zvOs}h{08EuV{jRL`78RW>b4{9j4|yMw*N6QDdws%c^M~GfT+jN$!>@hevG;n-nJ)m1YoU<=eKX1-?=|F5 zl+t+-?Z1pL;+cIyJ_a(eCX^A2&Tqzp(p^0y-qL0!gYtwop@j~AslRYl{cEXtHn<3eq0Hdwp%3b0+%tT|ET&(ll;%2DV5HK}Yx7{fQR z1&{rsNUaVV!wPSz*N1pyohZ*q$h*bQyzZGyu0766sP96La91<8je-NGo*Mxqj~Uge zr}l+qgm?ztVAx%SDYT0&3X#3lTsCq5bM52Z6CBot`Wb+sKOi6YK(*Mx+>8*F!{Dw{ z`kcNqrsmE%v1HC};FqJ*W8MBIN+S8RP(@_6=xPEUx>bvmIbP&vBRk;-j-J|kD zK}uup`8NSw8%89qJ;($s;NUIuzW@0Rpu(idKpK>q9d!ab0 zch;lBdkDwO z-W9KDn6><~(x|#tWfQdJ6Sf#0sT07NC5U|=cWrgAVZYCI&*~RX7ojP+!Z(a#Kk*EY zcH;QKAZQ?tFC4b6`RDa?Pk_gpfhRtv#`>F*ty}d0d~k{F6Y$`2iHo1}N;48xThLkP z;CSTQRz#Q_Jw~_W_F;IRB7_%91Iu&o83C$)f(?vE;uv-))09YcCbK9vOC9IP0JzSR zoQd91u6)v92?tcA$x}*lcFC4+482LwV6n;yn%;KM*YhR>rd)#S8{EFt9?qMZf4SstSX6I6Ha!76q_uO@AA3 zyFB3gn^w-WPo&1`&$Dn_xFrPK?mE0LhvCp^uoRAC3}&ZqIGR3GpPDIQVM2;D!uL#Q z1rzwmOjzC1ERx^-novjiDfFv8ymp5BzNM72Pb3DiYdf-x>{-QTzXjQ(6lWAy%lLFw z?Nsk5)@q;0*y=Lc#!xdVYPlDA+-FFHjvRPq#K?q!XhL26QZIu+K7>ES(ea~AcEe=J z-O0pm`JBc{xtZ%ilXbIWzw5V<96StOg2PFsjz~MS+V6%^jK7U;pzo%>4NgXo+lqa@cI^8nMDalPuI(x>1DDT#?(!dVX=$)|&V5%Lil82-5SVv-BcDi zs;$1qBX?~li{`k>$YDlz<)Gr^6Y83|wdBEq3Sr8_Z8(dD953{;6bDjIR8 z2*z2`voT>t|KaK?Bj%vKb&}V%v>c_%RiLyg;#nJtY85Pi2f$#@mmhSAJih#vhi5-2 z{pm)X6hFh|n(WmrO^3I<43vkyq=%gdw*DTtwtI+BPR6f)y=P?~BZjaMCV~|+31K+G z^fJ!}T_`&!!Tw_5c3x5!;`5l!?l+S$VylMugB$dWp=_&W`DrbaMZ8i++loaJCdzm4 zlSZ6jrqBmvRDK?c7`DkT8@dcHx7fa+DdV85NoDm>)Uz=vZ<5HWAJoJfgA*#=hkvUd zgQ+53Igi$EMk`BBt)N*qfW*vDkl@eO{_-iO_dMX)(W-u)M8Z2OQo;!=kRBka77R|e zoW+8z51*x+<(m~aWu*v&KEl$#FqkR-?3g5!6lnRXvHtc{ctJQ|BoyVUt9xtj&znF5 z8~j~&Ebgh}YQaD8|76&Zc)X~DO1UvC!DmSR7A?ygXshr~4&oW!!;|Lv8;)T%yiriy z)A{;1YxXIE>YXDRA`ZBnO@A!(wQFK?giA$~blxUU3BJ-<NyWzy`ysD7lfuz z_?~#qQb`K;x}$fYm?vn*^Nr({Qx*&}MW_-=Y$g!P2c8b{43?-7u5Qm>hZCou+9#rq zf)=Tt+YCW<*b8!&W3q;&RDiEjpadJ_{Wb!DoY}pbf~DZ4TQTIQ-&q12FHGpMd=#J% z2G_}m?Y5Ea%yqR>je&M$mx7{xjj}}_G5&ahe3Xygp@?IIXd5-~N%xqJf}gh0ghGmc zY$k*Up**;LRYKF3M%CY8;6AB2^sE-1 z!CbK93gPd*9`5H6==}EWa$i}>AEQ6eanjvO+pbNSQ!+kV9kO^9&e!Xq&J`1n(jIQV z!q=#auaN&MW;M9w5hoBaB7al_`=T=`o;g$`tO1X@U_;q7T*_MlNqKUqwL2yk1Zc_4|2k zw7sZ)=!@LtE5Bj7*35O7uUNfNt1u`~>dDh^K>%upVxnwj$72mshT5;Ha`)U1+M)0D zUp4556(!|>&ASZB@Zp+m$_rZoijjQL+2410N5UIm)UF1!ahAnlpdHto-tu4!B#$ps zI>K*sU`gq0YywjstBe>7Bdl}h(btZnd8dc8kxy_tn9N~wp7ApA03n*)$-&4-G1_-8 zKAK5iDXEnHrm*YbdNDF9-b}bNSQBW%GlkIZ+PuY+-JMXRX5-aa>W9Yh1`{k)b_@qM zS?pe4uo|3EWN=ykDl^Lq!@>DkA)A*UXT+hMv8j&hSA*~CV7Ic#m~bZES8Vimbxcrj zCTuHbm6lP7dJRD-Z5}UP6kGOtj`$5|>e>!juEs1)hK4SR^B9hulcc_xT*V9M%Tt~( z)5=+IF}}0qTuaI9k}P za=}P%;~xdk(Xkpgh2yMjTi4@hH@?go}%GFn(!8-}=I%e3r8bsc%PDGc%U5w@uVGp~8eFv~C}ep^|GiS629T3n-% zM}i#uHWrECQQU!?H!(HUFO1FD<@FJ|$!oQPex8VN+WPaNCRd@FO*agkva)3xDF2?eAk|Y ziH9dljGZC0(aB8Em2s-e)8!d?wcxrEqRHS4TJ6KA zt-FJxd27shkv*<$o~iI0Y*JurKdpM9R87<}hx+5&I|_uSjJo!709;DD{&@)s146*p z^UiB$JXAtekGYUwtX{BEI_MTPU^7hD-^-4`$eTZ&^ z=3pi!j~6Q7d>o3MqjqeW;#py>%xf^`Xi&5BlkV8Wuoh7rJvUZK~|zQS#EnF>+9eyI~~%EaK?QB zZMYa542B-^G7{>f+~rL#&13DVizlV8_-W3eF58;H&DXl%@V#8R?(%Yl`w%_{RfV|w zc0S(aSt8eX7v-TW^#kK$`w5A_^&lA0dBMdzF;F=u#n&;cx9lj#i$`2U#YB!yrUQxW z4BO*C0>tB>sf;Fw*qpasenxepA%c$*rA+ctOw{qT&Qcn4(qif8)BmX>c8nP*minZB zwM$|6zsAqq@EcP=5w7NEIB6cACnApn;J`3OAqI4^X<)1qzq;P)$z2oJH}=Z zSKUyUVH(3#ACOKsp~lKl&6H@r{2swg+3L8}Mf#F{D;>>IsP^EGvWS)h%Mrom4-R*q zrsRe#s;PdJ2XE-h^OogZq&$Sw_Jw1-F-y940pE8rCA<%1i|)&A+n{vTCmS<{5ngz;=puB2PlxZT+tNWM4JI8ZccgIZH{*8Xq=e5g=Jf|=$PdqV z^?u`>z_V$Ie2uXQwu^>bM`zyziw$uv^K6+`o;bvrfZz9e!rS=J{)h8m3v2Q3dI@I= zHF0)MdG+^}nCc_^YAZu`JJMSIh&C=t`Gp}Ju7-n+NQC7Wm^ll7Xoa6vIq>Sm*)LH4 z=ioQx2S)Q617@JsV6rnKdd^z%$$_(W-+4U_Ld#W()P16PCM`5JN!#+vyp3Q8W;AH6 z{Y%b9c&X>r4iEa;AB+Tv(%rlK+@4SV`SFmp!Zi6Ag7Hwd=e19Ww~Iob^3^>CqHj&q zr%?v;yx%5gv@LyYSf)N-{R*P@c@ZjahoP_`7qJPWR^Uipwk>9T3OE~tWt&J)rOP^eV>i zjd!X?Z2p>2D{+pyt#P5I*~~J0AWRl7bPta#$ZW<6KQm`C#g!wZ=5(sfN2|Mme&REv-`KwIGSgPPhrc%t`LqLulWpF`4oB2&a8NfU)~C#K&21bL`$Ou zjA4C0{x`mJo{SgE+hEl`*5!lv`=$wLP)?H+RD3@BImX~obb8CstpPy%k9O-ER|>z< zGhuJ^mZ3FXb$o{9rgq^9Pi~X?CPRgIoR`GlQquEM5KdGCISVJVcD;Oa7E|!fu?P4# zB|6SX#6Wi=5FB_N-q4cEVU#CUo*8Qp-P4r!u@Mxn+&m9{mNSz&c*PGhfj^52ss!4T z0d$@Dk8nH_-6&Vx7FisV)!RlhZyNPmYCOkpRKTw9^Ijin)H3rDqgefhi@D&#-fnfw~J| z=bE3jg60m>fT_CfgO}10M+NXiFdAX$ai?##llUW&Y zHv@K<*mM=U!kthd&%Ijv7rhZgJ$SRP*`lAuA3mrpidLIekqs>Zdy{?Pu#m*rEPwD+ z7aV{eJ=d8p{1e8FI`Z)CsW1wmZ0CCDyVWmvD~M+#P^Tvp7&k=6+6=#QB*lZ0Gdsjz zh$t!clw-HEDh595&nyep?ed>uR4~KXU2s^!Y{cSCaBy+mX!Evp?c`zRCms$Xo6}UI z|1D#e0rbzzT-{S`=rleGlFE&)&$+X+*5)u9AM5P!G!zSk&V+fo!sPz!+P&+YyUF-E zf&h2&72>WV#M>IMu!CKLgF`dw`{$HYD=o`Znvc9+aBYvLcz>qO z@})P{OZ`3%<$cXC3TrGZ!qtyC)G5MI z!snPeWZaAeN3@7cAWSC31a~p9@QJt;Pv2x>qs$l#66KE}Bg{;UMGJ4E8l@Ry+P!&$ z8{9|u!wma*^*pdbC1Y$V6XO{AX-61Fywxtr@gdY7d}j%y5W~P+yNnNoFYfMyPBLwI zn^O%P5>-t#OL4?VNshpbaEvwD7@P9IFLtP&(lm}p@N6#q>tCH3Wo8-Ss32XYoKrHV zH;rtXXz~K>doLj=*+i-3ltM$pG}TA&u)32e%@t5Xp6SaW3D&nFnV}%1Gq7<#>r;w@ zA+Vx{A`%L@36JY$r|?~f!b?|7u3G2q9Q1XS6IE6#2_D5eG1Y`|Ci}uG0o+97*$ zZKX<;C=s1`kqptm$0*{1;DT!~m|*PqxUz@CaA2>7<0Be}C&-mgkf}5yDMD^EGD^G= ziZjnm5aPpmzd|taVCQHI@K)b0d>8>3fu`E$Gi%0MLKycm)D!lsAgR5BdQ&waaj39Q9?ED55iPc$RU>I#Sc)@{iVSWlwWYqq534^GlX| z#?#K1XWaCU5sIwKO-#9Gr7C|F&l!z|`Qd?6I2GctC(ZoRvwd?C-8-hv;sf$>Q9ovr zntoNi)pE|&5e}@HS!U_gn$oI-090PMDsqH#`S;dY*A7lz=d0>jun&#Hud|h`o;$St zdG=S>pHX(B3yuRBv@CbRS+(PR!|P~JuhJRGDt&kverm2~(O8|ytEd6qTUX>IyJcMY z2rj^uG$qfdg5E}1&klCcR$d2dVJg*t%JrkKQP7(bmN1Tz{14Yd(Ym}|e$V5FI;1LJ zy8#poebhUDHD@Dwu#yrq`XI&43iT0#3X|xnfK3w0jsuSt3CuF)f}B@GM*PNrav(iM$QXM z(UvJjQ^$}RD5drBCuY8?%ZSf*K5MQgj1@efQQ@2> zNeCFm@U<@c3@7z#g|XfS06n)hJ%iz@dw3p<1oJ#^&IGrYZ@_?X*ifeDQ%>I~zeZ6! z^WonZ)$ZR}qN-b6uY=o}Cp@n;i~{uN6`hjvl_vU@;F$>X@)e`5=_1~!HE6P2cV*Iy z4_t;Xyn%Legtg_S8SWXzhyGftT+f0nbc8>ia|)!&)*97{XFwTFhcCb=8ShA?T)bl- zUJPfJ>{+#?&6#7VPa~76D8KvlEl)5O&sWx|tm+Bsm4ADHZ#Ec$Fj{9;^j&(-Hz$DM zWPp1Rp5x<;F2_JV%6h^tY7_DZ7nbx={Idx$SdH5hX8dQ_p?1QDcRa7F+Ty438P&kb z;Gw_zKlEm}$RQrCE=u@d9Y0EFpHeRUGDYMF6zkUaUV9i`;+z$x+J-MOY~q>6o!gON zw!A^D#y>g;_Z*4=V|9Sy$$iFocDXlH_AZG?x$-hF;{hY+8*&kzliWVa#4D-{E^;t0 zH%hUhDPw!*gjXZb93*o#c=P=DqSaOAVvN$uNa+rvbFuvPqtD17yaovd41QcXm*g+J z!N`o`#Qd_8)XFT?@bzjS1f#2|%pO=zL zGgoHj41$G%vg+Z`*&<%QHX_Xr+Rt} zzkv(qJ)7{)oVwmfEH9ZOdWbjXI0AAwUVrV!MOLjqj>&4va_<}NTU9dn3md%7F@FI% z&!dk`f}T@0AudO<5mezYCoM9HMip~7ngczACHmbVAETs9kn2BvFe45J%*}uczgC^L zGj6BqJhQnAPo)_3=~P(64s~nzp$nF(Ed^SibCQII#Me>wl{UZ(Gl5_{;*d0M%&IKm zCVt^n#K$J$y=jib-8Yguw$P%&@m~FE#AD9@eu8hkhpDT)<~e$y`|JAdpQ9ES%n9>_ z3wvvtFOD`7Kq`NPAA0k=HIZD>;GM_AxJ}5iREG+_jgPGA8+tTzY!tN%UU~yh&q_Zq z*MDh@{;5-M8C7Aq**RY<_|iEl;7*UM%Dnb?)Rm}!hdl;N=@=4nSrCXrD9p88l3m;P z{3yRn;e5WsL5P?qlUB9wya&pDYbU;4kxHAzhf`j$#|`@Z^Ih*<*+Vj2ewNT&M;OW` zxq;ikA7x}nNVpCAy|>iKma}m&rF{|cZYpbCto6~nM@ulhR00opF^CNvXkrVxb7p)a zs>$6M4%VDmc|`Xj59{|$nx5L$NCE0XmyqMZK)+;!%HyxF!TAjG6L3sCUig+jqFG#M9^Mti4_F zSTA*-3Evpn$c7TIo!vJ4BC^q-BH*k)+U7ml-y4#rF8ww6#=B$Cgxz3mUGc*ZrHCzm z>oMd^CD#DYIKG8A6n_@cMboHYdJF~Z9lI!fupC*C9lrYT)0Oz4N| zJF`45!Aaid8Lw&|UiT?1yJGJ`G{lQr1qJ_}BbK?*-}m{|&rViV$U zKvy$U)m_*HpHaRkq!1V1)$izUv~Z|ybQ3;fu#EsMqqI7P5t}oH@IvX^#P2ENDTL_V z$8)Gx_!sx)Fj=pDWoe%NCnv&f4BonD0Y8L;enVqV`ZPC$$KwBIC3=n@EeNo*mZxq5 z-6%40RbE^6?A*3NNr;S0!R|GjR*wVE@Ev)GO4AoR&M87GhRAXNCalSSb)LO3Q&sMa zG^G#ERgo;O^wuWh?ohxvDL2?=fk)w^W5=CiCo;xPa+=Hyf!Lhhb;^-Z;~5MPhgA@B!3K znqc@@LC{OQcX`Gftl9CIvKN~+=AX?;5qjOY0=-~@Xa=i#W+$N43p{;r^E}sBGhqZD z=AsZxm{AA>UmR2T3Qt%DH~;$PwcD|2t#`TSvrZrrub!6oJ=IqcwXMrSt0_E83_duJ72eiWHz|YKg&(+@GL^R^6a?#Lviecmr;ceJIDkWp z;Wp$=G3Dcx^J4W4K1N{}z0G_^x%GQ?!6uk<^c~z34!gU46C6fg&&zH8rxC~$UzHsB z9`7&2Hps8;`hA_ZJ>|!>4o3K|AH|x_GKTYt!18iD;K#^=qatL*U3^PgzzyQ9E2YBJNHpd@;CpH?cK(kSL z^&P&3pDQ+on#XQp002M$Nkl3XShWW}D*78iE zd;J*?K7&Q3U!=Uf@LQs^BOP&ZRNs`ex*iwn$y_6UmE#1cL!DD? z@L-yOuzEb72cF9-lh4<`^!>eDD_AilrI(U)QJoazyf-%Qd;}*ZY%)Cj+P zRx?)by_cJ4s%QLeqY(;t)@M_M`V$8d0m@U0JWEP!h!-rpXE3J8dR9LxZ&EpRGj&#d zF;*j(ZfXY;S{T?VD&@dHr9TL4(wI>QBl=j|MfWpkO>+9#$Eb)how$78R^Ci!Cr#_T z!Mw*eCIBl__{EX1vk@#owi(P>H}j0gs7G)VT;~}Av{}P!t0MJQsTm@74$UxObvk-N z$YAII_zcXe6ap-xoweCETg~|;)jgw&nPcG%&f195_+#iyc|}V`1>I~zt*r1BW$4@) zD|DWj5ri-4N=ExyD@}GIvDNUc^C*mw{1B@?{Ag&LqEFyW7+O67R>#^28{tKx7fOj@ zpSCE6@!Tj!^_358*Sl~P{P+<~O0P5Z1%FVE!8x=^DGkJ+UX}7V4CNym&icVY)8CBr zgc>q3<*ZLZoxu_ox(^TY&Vj%<&tm{;K$XA6&qh1?q-UdeQ@&sWJx;Z^VwMWyjS@Z6 zS1t0UIbGJcYiotBE|hp3943VEigR}u6%qX5#}w3To*VwG4-aT2TlDg5+6yl7flq3A z`o^HqZS|x+uLd8kp_g$mS?slhUWhHCE+2o zJhXD|ve1(mpxJ;pcfrK)Bv&go`kE_5tB3OT33sc)4YTNq&-4?fDSS!Met`D?a+QB~ zP3Fp!IJe5nX<_%Dm5N71Wi@S|! z8(6m1ypT#eH+!pPeQk=V&N*^c|4kG@lJ)XuG%fF>T&%X|4ivLn&!(q&fk+5DS%r~w z%sS~9fgsyXPJm4od9B^y;zj3|WFGQvZ1rB1m3U|DOgJ$(#Sp#4l`+KW{Kng9Yb&L| zxFB?}Sal=hK>^Q(kD*w=RR)r`Nj1BODK!E@Nqgm8Qw__TMm?X_p7s1fL#M(Cx37OP z+q*F`&xWcu0x_NmV@@A%S-v24of7@(}clH{8vnGBUz1QG_j88glnQ$#%7&UF%g~O2{!W$v+0Vg zR!b1~H<-~erNSmqYNI|)76#)QmVw0-G9-i#VIrZhLvKntc2vY1a+ZAD#RGFrQX)bs zobR%?o0XDH@!79n1DMIn8^cv|Ji%gTtbGh7N@rvg4(&e>u5fkNzs!MtfI7l@i@jeB!+KQ3`SMmswB8=V-5j3~oSZD&0(Ym% z3f0*VHfgXxW-}n!t1uES5@0VhCp+MPq6t+FjnK7xr3>+)TOAC}?|r{<<2_Y}?WZQs?;k&7An zlt-r8fw#G&r9-BU$xwp4a0~w~MnoB9=WCVWwT+VHcWGOZX?s$>_8)raJzj`cr()Vw zMeyQEOSl{u<jLIU`m(n7EN&oy{Buua#m==LOGr@}od1S6=Wc8${vU zstJNpU8T+H5Wl)#*ZFE_Rort*X7FFvHxtGBW+VK4?I)m< zd2Rlq*KB*Qu6?{Bl)wKg90Q5=XSjy~}r3X)dOAxUu$FA>l&romKw6zIA-MUoeWOhYCLOaI!I9#R-_1#x z^kD#uG~U8rS;wCWfbnkQyE*u)-}PK~m$!ykxTHGgh>M=_q8SXKIqD|;52?YJQS?lS z_@*rv^^390z%V}bXFVY{J}HxQax){r08vloX?Ss7d=fs4WEq?Z)5Zg8527YGrU5Q_$S@p&$33xcT@o~@1HQhI1Zy_^2u^XAe zvDV+{a#_AxJ~_LpcP=!Yq+sw31lK6}hdl8rI&);_A}dp-Z|Qg*_QPk$dH5}7=?FIY z2kE6fG@#tHrw&>B6Fo>&c) zr@X1?`m0R_GP=d!nd{mJr(=M_Tlm|zHgY zGdx87WNAQ)Mzw_ZK@FMkOz|hIV%u?dFhm);D%{V?rtTY(XE16l_-99Rb(q){5`@EQ z>UTz6{lRbP=p265{WuS$t6%KDBm%vTgeZw7?T!3xIOFsQX`RQL*U5?F$>|MCNCPZ* z`@nQG#YgL$7|hpR6?+Wb=3yAYb#wx#_%x5>7+o*gTp30xQRB6>PLcv@U9vmv%%9RE z&s81p)&CI|cHRT_j2Iit+P! ztx%X()5Qn`Y=71H@oly-Y2;uf@jwK%>*K2BkjG-pPA{}PlM)r~@Dg&K_P)5D(T3qL z0*-+d-pQ?|IcqrCXy>y2T2sw9gso*)m}zH;TSCfU5keyXK^U+Ta6A#}s&y42s2g>c0<)(1 zt`!=`JXvd&@e@HYu{Eb@)>k4DCI`1cA8ys>_lh5@b|`a`odZMA38g`sk;`nzVqB%9 z9KC~dcBP_abumy300VV9BaOr=)L0HWa}7P!-YGnvl5j0aKXkMtQMJquz0rQ&ZdQNQjE#!3 z>8+mK<_Lwcnl4;X`$9x)sAP#l>unmxO-0JpScQGe`cSr5|-hT0NFJIk}a35T=Mai8A}xzT*uMC+^{ zcfs+}GXmfBLYRPB=k}^PnP99Oen+6#w4C^OdAWS} z^1}dnkR$lOPWr-X5wO12KivmSf;e@}kca?D^JvZ3chB+El=E0U9@7aAb+m6`1jMr~ zBWBUwl$YFZKV?yF%67jt=H@G1?@ifnP!QztdO{wATV}@Bw`oToMyb|0%qN_2ACvHK z__jx9iy&so`mDImq0<+6GF)~WMCPoWC_|9WQ;JrvTz9G+WL|_&n}QIy+LFY(H?ImUQ3`oowXr+4S|TO2+rO;cm?rNf8#RoL7S0qGi?!5? zXNq>@dgu{xz{CUMq0ad()#+WD&LKJLV_$$MF|W*Szfs;+{ZLjZrG18IRvHW=qyz|P z`51$Ms+sVcR$b>ccoAT%&iaB+2pMnIn(a27rzB;eoXQs-W;9Yp3YI6%6Pt5QM#*Qm z%4Zy;Uz7&_6pf5R)=m=n>Vb>un^8qD-f~glt@=hF%LO*x)=a9NI@3LzP*&56GFm)& z)@qs6!!DNEiknj}K$w@cSA0TmmABmQ)X0F*r54u%+*14uTF99{4VggI7lYOJiAf0e-Y&|@|;9ALMrPe6G{t5xl z9*xqSt#4Fe>E@xG%ONe<>$(0Eqp>u2CU*P2DE&pao3sAquV;0+JoT-2DXX6WU6pD- zRaUOG-R{HrFmw6fW^mBmT!cfKGU==Jwz4CR{se%JFI}1Z>VI?*{7HIfbG0~?sXh7U z+2_hUD*p4_qS$-ADB2mqr3e5#Z|NDKqu3)>)^5BD%)qM-8(y8JgXeLot?5E2V!S!; zDT6${m=aM8w^NfLB*u-a#!E|q%$u3|UDJbKb)9AfMuwo*!|@EPi@qpClvD>)UStC6 z6^5^a!OIw&_IKB^xPiUA=Xq=xWx@#&e8LMU^0Q6|6d(6OXN(`~y!$sD4%AgS6E3|= zC@>_YH8hO4F&SD}X~9xu_4?l;DvCd=KYdcx(}HfG0-@TDK?{rsL(HB_!=NJNlb{TwEq;kjN8?yI9@ zo|Qg(qt&sI)hOhIT|D1Nha4N(OaOf1b00yM!$rv79U4e3g5n z+JjrKTA~{C$h*dc}Ixi+Fzc+%Jyf+GZ zR;S>lY3=yFFeWCfo~WdihwFh(l55;@uO!yE4Vw zA_fpoTql4H=;JrCFNyJ8d_|)o+VNw`}VxUVQg{Ak3&OvSELG$(elwhhK z4lgs*9Kov|{WA`jVQH=1Wr%n_v=}dE;*t7a2qM!n-U^THzBsMp1?F%r49$BFAG2zu ze6emx^?j7>${0MdZ4@wYhcm*V-;gN=E4(rMM6{#FOgcWqM$1(j;W;A07r~gC31B!| z&)tXbXa+_jf?AxjdV*!G&qRD$MN{ESO_|faGh*^5n2>f~Ji2ZVZW8;@VMM_YQ1)P6 z`6g?=E%t{MHQ)cEKVJUl-~AWWeOx}w^E-cjSib-Ie)&)St&f)Po_?_W)4%`! zEH@wi`Q=}q{Rhi;fBpILkN@cRmmmIvKV1If-|Xn){l8fL^7npqMrUt}mH)Z#y zcRuJHwkg+?^`Y?2`2LJhd^_jwrwl1Sa~v~)I(Sa8n)pjeQheqfghd&+w-kp}XxWI% z!jkhYTVcNB>M|v4@&FGA3GxsIMT#7fAYzh`7zdOA z8@Zi!$3vTLdbGQ%x~5xqzW2<0KfirWIYfZEt9AFi|MTDfz1R4g*KhsST5||_>79Gb z(gm^k@QQW^rp@!)f{9R@>)j&1i(r9iF3tp;5|8MA5m$WC?OSv>)D2(pucURxp=5 z{zwMYUctv(D^P~lt?plWjV!>!+J{>DfN${X#d?j5(yo4bHtD>&Oi*4|c$zCbu2*@K zGmP3j7*y}%^W~Dn`^lnC_m!g!qk(&&nrV{^AMSeY!k&+g)i_!{fE&6Y1mjwL(twR5 zoK<#)yW{c*!I&Rmh-NuWpzh4@t>0NPoC_9a5Da4hjR3(A36jcLcTMDO$a)XkV=dGU zqPkVr%K-+9u*4}Z`RO+ZqF{`W4>CnJnm4e5Z@Z16b`tg&4e*t~0W!;~YY#Vo-!r*b zy$;-nyjDW5JYsP^^;dbiBt+1P*jW`TUcSS!pCxdP642xSuPt_^w2gB)s#ihvtIRBP z${>2~8K(!9j@ZkK_|(Q9NvVt_3xwc1aEw9vrclt#l+nAU{rXMl_dMVMy=sjvxB>a=zw*ZN#sBN; z%RBEsU7kJZY~7-s9tZ0$zW-o3d#90~7C-CLNbOnzT2#i-}QqngxbpA8;xXne<|bR?|$vu%jceKF5kZY?(#=}{4>idue~@l`al21JIk&8 zyURyE_R{j&m4oFIH+Pm7grn6qlf=l7i;|#-DuYR)G?)kN`W1yG(MxuLGs}taL+e2j z`sM@nLgsny#M2h2?V|03Bpx^oo1ck~?$xQRv?hjiOi zQw}Ce;n?W!q`bBV$EOw~f))FJkPpj@kJ^vED2u;8ww@bQj5 z&wNSG8oKh40kKTT-GCs2W$WQ&=<&XWle{{KW z{p#}U+1=&IJ8vzY{oLo5uYdU~%bQ>N_OkIf`h9s_qj9y#`oI49>S^uB_U)IJJ1@Ps z+<5iY@(XXgxcsS~{^{lJ@xk)`-6!o!zn>Le+v_*6Srs@M=w$7eB!@!Fw42W_=2`h=Y01&5VMh86#W> z6E-;$#J#tg_h6ovW6I~cS&fhgApd&}9eLL`!QDR-$ml*~PCs^PGuP~KjfQ)2k@tWl zz+miQ^p_Gl;!Z;sDWMX95LrOpOu7-UwOC#Vnb!9;Jyd`1$bjkh+81k8Mv#wUkhV)! z`RDuMx92GwZ5bwt;)b}hSnivHXW-2 zGeqzGr3E)zt0n;0B1nXbVH%jgThB~As2oTxi$yzzndDJKItwR8Q4kc>RVj>vqmY*v zYJw)B1!qItggKkXf-VHfeT0d|y^%oY*R053-TMk*tr8!nF_o=hP5LAf%9{%(qUokK zg{WafmRJu;BLz!m$^AkZ99G_5g4`ZA1e}9KWFD&|ArC$$79Mpd^5!fyGd$*_t~Z1w z>pC;=yoihnuSK;xrm}apCO#o94)6_1b5cyJVSG=t@5zeE*W)pBdq_d`-woQ zRD_I{^;$*jC&06)2acRenn4-dr{hCE)4nW7Y8@j!tuI1pO(GH;qH!XFX^*lAEAPxX zz=nxOj5It>vXpesnqPpmdYu=e8)w6=cL@FPHlTlT*<9!S~stwvhhT)#b?{Xa>*hTVor2W$C`Hb2-*^`7 z|MoYRH@^Di zaO#6M(6b9F_1bOlMuT`K4rD0x?x;vK0TUzqn`st|zyhC9JkqIJ{EAFYbKT%;*tT0K zV`VdAYZg1AZA~0|p?BeY+=X?F3r5?kuKH_lBbUo?=i%o@IBnceo_MkLYqxy~c@Io* z0}eOgd2=S_Kdb;py9~*h8;ZZS9B0{TH4EKF2v>g@>vx|SQuWij`ZCuLvUkd_GVkV@ zbQj6yyLZ|k^5!zH1d1tp6<*})f%KFJqE^;0s5-pp+t>QX&4;?4o3Nj@)g*gg?6D{! zAd_ue4C^k0glauP2_aKIPXh`izHZBkq$A3haVB6Q2m%)!&*?YG725eYjXY5-y(?=u z4DhlbtB+anEf zcLLp{SA7`whLVzEvkJ2MXQWY?<9;E?8$LP~ZH=g@?gY-db7lM4KPd{REe+1|DW2?P z6-Csx*VS%uG?<9dYCGgr8P-3p9)gt8z#!h0o*QU0rE(lIK10z7PGN%JcsX*S$i}B8 zeQpR!jF*IqYy?P*({1q;as*zSP&BDRaE530_XBGYSiXJKUtQPa4BRiD;FX|L7^4s-p&W3x}1TGBPsI z=SPp9Er0uOe|Gu8-}&nD@~2*2-g)@AG^H0?EhJZ4Cx|>@T6&9h4!`zrdF>DX?&Y8U zxBl4j+MV0WZ+-Fe%YXkD|Hkt6uYbJ?uP)a=`r@+viQk)lzo@DN`{A>2(D}0lHi@<{?Grl<>&v(|FZlG|I$Cd{F8s=k4$-Q1rB!3jmD zppHv)wrsijT{(@g=2*Xq-AUn@8-cS#a4-q(88LelTmo&j@6NR`ZHacbqC3{J0LPOi znd|+yct-J~S$GJ)gg#d&9QMCwTx!Fer857q8YHwYTC_JjAJkW{CG%eaF)nD_mzpR-a<8PWSxYzUv*E26omzvsO!;aaBK z@8MF*bGd5XG?W+}#*TRr-<;S}U&P7UHBTHh0XZXp2tuUFdA>dt-NeWS96Mg_H{`WL z<%AUW?zByq95!NmguzP!WCU)4j>Z5(LYScK*ANTAWkP1BwCdTp+M=97Hn)qm*lx(a zS~~;Uv1Ej3a{t6iJrrY5Tl>Y5LFDjYuMuq>H&74ke?Muor{_}5BxJ(dLEGbY#!OL_ zXpAE+o2+>L?GyVZ5L7MHI?5`i4E4k5SS)rHU$0|`VA4lu>c*V%6agcsAqv8u6;s~< zM*f&hghh^kcrGGTf4Hz6ZW$9Mv1=d8#>M`pKLg(Uuf&xd4gTfPa;te6AnRcx{Pk-; zn>C&GsRJxevXnK;l61HxT$N!&b1u?})<_Y_J#qLcgPF{(j`1niXNrK(A#z;9(kZ9J zMfDb;R9!|sL`H1gr~-}{7cqEU{q~!0FF*U&|ITvz`))7qK1$H{KHo@f!AQ&lFv{;A z98aDG^Fkbd@=yJV<%d4={mbFqHZB^v5FdL+U8j(q7}%t4>*b^ z>@@&85Mvk+L1g85 zhWR`j*H^jCHJsIknGlY~Z0S+!`^`@uUdm6|Yn zkF~ROWacUylM$6WCv=4aC$_Py%_WRtD!Fjine-L5Tru1;LT=|14Tg@2s?KO@T!Xeg zFCT6n!Zwr~h^W-ncXU=`*Pnhbr9;)n4s?) z!GiH+zh@$|d^D>jeUI036ZX@u8g~*w@~%Rky{`${IUznE48g(>WcPb70mc{1?}jkg zcdWprt%!H_+S8>pCjYvL_zA8+J|8F(xsf0ri#5NTp(cXr{e)@td@R$5Py~rHR?i;A zj8_u+)o&0y-pqiGzGg6z)KjO?0TFT;Ww8E=VTO2F?hvZSL}5&ML9nb3zH$3;oW|kf zuOGjBV-L@=%%6^n#X5lcmJ4TAj}nr_bl3Un$3)s(>6ItS$NeC`vgc?w!aL66aM(Ne3m8A)1f>T@9e+bzK!lWh(&(pO(IHlX(O$_}70UKh&3jsZQ{B#dZ zBBD&Dn)o)#5OF~`O&MyBF=tUtNI9#Gs~LvB!h3aKXzZt@^|)_1%=+fK2@5NP)rm<^ zR5(qF&VldYlmf8F&AQ~ix54vkU-|m7y!YEl*&hu)3FZXcFc$x6IhND>>is);@OZiL zLqE3s^gr`YbqM#-^2KQ6!d*)NrEbGiA0H{kDz~4wiZ49Y5fgkWX|9-ZR;CQj7ZEYaf@0%Q0P#Kr;)*E)82;*E6X|a zHru?NzuaIDJ)eMkDqwqd%TY!_1{dYdS8X1pUR8uBpZ+Fes&*Vyx1%|gu`TETj*N)o zwb@_}d@ID_v^A*_M5EoTSA|C*mv(Kla$arns=2kujTEdZ{cMcKIyzv_lLip zo3Ph4eSmaqI=;7nWr)bVL0H&@&uLY&$3{$)XddgwaE?iMTD%t8MOYhF&}GR|0CQkM z0G#9YA}re=(DbEfK}bkT=n1r11s7t%5Ft1rItx*PqZn*kw$5^QilBcZL~0+TZ?=@* z#!xwd2Hq@SG6qpu{Kcv=5lT2)jAK{|srEECu~{h*`T8D`2GyAv<9dk+9khb*H$?O# zG##vRmI8`h>sNn=QA?n2l_PDnj>WPQe0F+&%xcrO{v0Lb#N{6~S`d;tLx1#9K9<&m zU;*eTr$iSbs6$g*MlRy2RE&u+X`L~zXKJu~Uo8P{-{J<+W28Y|h z1Kl1*ldblv7NZ&ctqJ{^9Ua5QQTq_>ZeM9jjE8kAfwtFigSn{=s@oyu&pN`AYfIk! zcnW;@-VwIxpNg&RK{hat*DqK)6odgJ4&6E!3#Dcsn&^?6mAmCMB|LL66@XTDCx{?g zsvK)&wzEnmF^u`8~Kk$Rlj?mwU+KL z1oJ%dr#!)7gaQF`>6cF-u9wdCd@kS0Tid+emkzxs>Vm%KcaPBKF#K4m@T6`pzPIbL zHQ!q)|1Y`q!Ns~Dr0GsE!VRnv!w3cAd5VF$jY0@wz=q1PW+NU}EFmaLB+(I4Bg%jv z^h=}$SIJS)2L=eU^>0YnY{F&rp%XHzLcw6HN<&cEf6GA%!LS;tpv)u5xolW&>+qRS zyY!C0${Ly&@ne?yCPYy5z|)3?PpU^h24SUDB%Belo{50i$qG5okABw2_e^*qvS2j+ z{Mu)A0D-bvR|IaKtdQ-7q}v2}%y-fPo|E(Y!7E`?ohHC-dNn>GO2QuL;DwlbZq2e$ z4O?DY_`+@3%%!EbHFG{*`*lH@6)g{sDhs-*`Vbka8bREHaj{f3btWErqdwAmuqFOI zu|QAOhe~bKVeJ6d%y?4ZG5pyh9c~-`%&5pn%yRp(&b3;3Zdq<{66{1l^=)8_FN z4?r(mm_^}at~bJHMAN?PXF`Q*BrEu&bs{Do&3QPz%Gt_bcGhq6Q6o#LW1^T6mJ9^K zUzCr3aDoTOJjMnwM)=x}0!2@}J{s|?2I~uV42+P2Ce`Oesd(U+D`oOCq-yf;f3;Mf zXfR6LB)Kxjh1U~@tXTgt0H7mF$Eb72%bwrr3l`>x7?3Dkzq=WF)gF-j9{jo<|dLOnVSxP8mpJ(}fmA^3eOXB0xO z0gynLwCVw%()$c^N$*)7Z5b=KJUUaqBA$!;zCPqtOP|lBiLN|cemc@fr1w^Pj-_5Z zBqY!Y`l~S+7%Oj1-fFGDoY6{1Af_(D-SE| znq62JuGJe7E0zLMIkiKjtkk5wX;U~rI}XM9{BQim^5r+~E>{xhhfc#_q1U#JD+$L0 z!0xR(%SS%-iTLv2@(+IYZ!XI>-d^@!`&5(QjT*s##pMK?F7q82d!j^YE+fR8j#1^h zC|3|dO#GUez^<*&OvLAR7d8Ih{e_=d{+)m0UoR~4QOe|Y z#g#u{!&M0eV)AVRVoik6r{gjKa61KxtI*!5eRJv?AO2R`O?Ymi5|7R3DM*@x201Pt zip2KanH#;SqTU?UwvkmM(1}S`W>IZ8Z$i`lP)3u~K|tzbF&sQMF&$6axs+g;V1Y?d zx%HJ|wuM7UhkpXz@E;+!-Mp1(vRd7zk!#(FTc*!*(p3GSpalZxJ69C)IjX!lA+1IN zyAep;t&I3cUB3IbnSsDvhmu6KO{lN_M7Kk8)ip8$#qeiguU}F2L=$C{m)Y1{|3-mU044c+_3J|jp%w8kq}_V zNBKrAi3~3C;ZqDUlQtekgxoI}#XDsuC}%<{S<8{-YI&@p#8`tuF$wDQ zoppkiK$?`{s#Sq7h6ucmnrFh)kY8oJg6mlt&e}kP{kG~*bc}NN%V!8mwuJ1V4aDg* z>B!!27=u~?$yzzJbojz2Zp9@*JHk#mWj^1cJhDcri?xjdki7+D?VLYMq-_+$x*K8D z^xGN1;70M(;?f4g%5xY7VULjv*;mHHKJ~u3XXreAX$(_3BQf>Oj*+aT@)?mSfC679 z;lkYr{GJy(zL7<}Mlum2!Mt*|i;-slXd|U&_+PO=GVSAzn3{U0O_&K;{PHjT#`0(W zt3TiG6n9NOZK0IO?DIBQvR%O6`_c=d(l=Ehk%Iy}oW&JlFG?rV! zosU;EhgGT+w{3E#`ZjK(=!r-njuTN7J>lIE8jVtF^x9AwyH&d5+Bs;`t-?A-Qe~`Q zJR+n^3GfDFR#B~KX+DZJ?x}0o9qRV=oMb0foi89 z8`uWx+SA{IcxCpI3C4q?(dOYOYjfr@5;$W8D>Jm99X@vmGS4F-Cz2Vwr4CA4nS~0Y zPa(GfC%J?^jJg>g(OvN2lJV5xDqOc=yZVAb<)H(()aThA1;3Ym+o(sqwP}umtiIXf zss0F$zI0hZcEtpPIDipSy^|*eARM!fPB{}yF|?Z@=UIF-aFMU}7%}eFzO9szxh-ws zI7Q!cDTY9%WHoJy^{1=$lyu6R72EOF3byFQ^SQJoh0bLa%qIUluMF)${{vjVgX0MP z^2}8m!sTdRZE!!rzdTbBWL>7c$nsr&Rl@VS04c(0E82{gm?dKMQC+}A|1&)8osg>C zIuf2%rWy8)kYG4Q;>OtI{)mgAZw*0<0AeV9L?VgMHxV^iDv0MiG>q$p5*F@hQ?bpn z$II?fh{|8g-z###Qtqt;BHMYV;U)1)cphYFI7Hln5Po=qFK=_i9YSMtL&zcP+Q4_e zMmC@aqA~%f`D2>L5V-(G*=uiQvsw&!8OQ3~gAHrX+{Wol2=+T>+)C>@i`8&i zxyrI=YAissTt3X}KMP-UjPOZ^U!YA98I%jGWK&Ms6&~dZwi6yYuAY=$Wp9^P9rd*S z^j>?LFBuGW@8l%Ab~+RHY+N_!qO`#qj`p|++wTah#q3*&asU0h%g_AfzquU#_Q`VV zg{|eD$22hl&n6$Zcja_*tt&M-`+HS`Ao(e1h04x(P4&&SzHhX7uymewpDa zphjc&3gf(a=ab7{{F%SH{P>Uj;BxckwdLK1PnKH;ZP!uRGjU2u1}O^CJc_w43!si# znPqzlJk;`}-k~-4U=)-$TH&79O1LK42Hz7whVIbdMk9%Ykv*sOqoXGYL&|H`w)9?F z!rB5UK6>Gk0br#GBMrr4yzsN{om%RavGWj2&(IQA%@}aqjgSuJ0uW|#Rpn6@c$<}L z4a*oD^3hpv&3!rtqSsI5pske$C<=U5UNq}nb!Bt$2j9V=%HXn*M^(*DnV9Hv- z)qSsP+gy|?AG12=k8(@k^F3V8QP_iY^jt?pz5vfG_z58Vu^1g*hxzp-gt5M77dw!S zcnn_&7AG*7JlrDmAsrzm@Mg(RAkS+{9g; z%n)5$27i^C;Afu;eH$yS@{R~ubyX+fTcv$#Ct`{rO2e{3OcqJWC$`vmvs^^nE=hy= z*izL*tYM>33eqR+DH^zSV%E$v)GwPtZJy`PO`4n*cDh;>377#XB91g`2Ums3TF=4K z#~>JE3~gb+o+xidJGDt<1@{WL63DakouVqK&$tVMffniutJ+@db6!9R5INUHZ>HYa z_Bc4P011Fz1Z{#Gg91?hFTM@+YxD>okxGX7CG9)qlQ25r&##S=kLwH^rw?m+BCR$O zvQu|yv^Oa9G1rpd3Xl|lN!efdg)c8JzH)teFI;cmy1i^4WX)f{nXG?dIez?TdFxj` zzns6FZ~W!k^_115us(!Hmt7L=F~c2SFj$kF!K#QSlx2MZT07h?{q$Rmd15}L^emh` zJI-P}X%n}$k2pRGR&Av)gay(3`PZAgz0u_CI}a*+g&85fb0u8A{k7$*U;F0ry9@I? za;`x>_hjAL=ioc*J>Vg~^Jz-a9#FH_eXs(v)eQ>Y(~UA#a{PymjYL?y42{5*F%X@N z(NK9%G5v!3u71w}-F?7u;rN~3@d7-D~7IteBd~C4Rv0kNc#|V2Ie)2My zGD=wfM>Q~Xj$tv>7EdsqX1pBFGI9iC;yt0m^pzFrQ)Xq&1ap9~&Bko=Aj&4WG7J7# z=G7YohFKJqk=fh^!w374CxSC*BqP&Nc%{_NLLV5@P(pd;)dEXdPrBuAL@S5(3n6jL$^^D+7WU8!&89ds6H*zvFg9vpYbG z&=AjP^|W|a$4u5H(2toNxqS8xyd!912wI+kA=bnRo6t0o6}i@)@~Ata z#ava|ZG_cxvWdX7HLIfGY=oI1Cq!3*$wUNZ_)p;siZ2jN;8O$y<*Y7cxkhlW24LWzKPlYu<1IzbwhEPm`Ded<8KSm^xt3Cb0#wZ3*y zisY`oY6_uYH_|dEAXbZTXaOw3n^YFp9Tx8Ri@(%X1ub-^Jeb=a{A5+g{klVGYz1`$3c9OZbh7 zDiX=0;FT;!7CyW`YhlH`$E_)N)P(I}9cqNSJjri=r7at-hxum-{Cb{6G#jm5z4Phi zzy6DVZTXLX^v42OFh=&Z3B626YwRUb5^JM==UniUO6^Ob3CI-&6E1+M7b;Ba^|D( zD`l-+;Q>_~(L|+)F|VW8+M*E*N+|A_(e7#J8^JLPt4hOiH3`b1KYenzJZ+N-{EhFHKbtb4tT5`>0X?(K-9o9^QkLb4`4eFJ zba}`-L&h*3<7@Z0XNKv5ZS@Z@3>R5+$Kxhz_#Iq~ST-6e zuWmLGGCixDvBH8D8Yd{Nwb(52S!svHgD;s*+sx<%7V5l_iO45{c82QOlOSSY9|r># z<6fbLzxwO{=kjy^$LE)WJH>y$@Y1r^nYWMM`quL7>67K~e)Bp{Qt%yEac(3vJXA|4 zT68o+=U~HSlwh<^l5KIVxK2G+Ut`2zC*;wnZMQvjOomz=Yh?JeQP^=bwI6uw5r5w1 zY1^%aJ_=rk;m+0_L8X@op#lmPmV7S7dB)|_yNTO><4eoe-}vTo=gzh8B0Q7A4=$B4 zcnmh{na=2=<{OnpuQS<*4ibhCyc0m5B}^wgG8m#m3-btZuC=3-!YVW=-^v+~VJupI4_jri3dHSG|4iq}!S3Tr}If!`{RnI&?ZKDJ!KXYi@Ro2eH=g?5JhSVtm@F{hDGFMg2)nj`R z(Mhuzq5ffp;Yv8DkV~QJF??ZI7k?FlbqJ^X>cCHud?55m2l+ED=e>N>TTG_&%IMLI zK0qOXKdzu|gEzcQ=cXgwt7F=yUgPeefWCm13MqD&dmRuVVOP87$k->p30AXLv1Oin)%le->99?wK= zh>%B+qCg;XI0^8s1~)h{hqe&qKr1*k5&PwwX?PvEpo@%GK@ z%d@>45tTCzca|Trx#_4tH(FofcBR%-$MzLZSbfpbvl-DqKF#pj>`=Z zMr-gX0OCB?lgWNQcfvnC5RX?buvGtQuu$qN-uDlafB*nM07*naRA)tSJT6Rm()35v z4=Y{V{A`@2;?h?@aH%{a?TpNg=us1^w6=L4oi$NCF0Im1;h_fAs>mUYCDgoBKb6R*s~j9nX};pso?(u%qXm8_$LDT``|0HfRCDDB!Z z9$y!zQA12AcDC;itiBi(A&r37h8l#ZSnyjhwC4mBOUDEe3jnn`kCY%q+6c5_#(gp_ zp$JSecFDL9w5d90xq8feX+=zAO2SR}TeM#RS-s89+x2loSWQN74tOAFEebJTpr>Xx zHrp!UDCNjUY<2Hk=p>@hTa%as5MSu}?ICfTcQr;3;iKBI8u+*|vQNZXmI`8}^)JM* z_U6(2t&K9~4jT7VdGvM)Q(y2XNB@G|q}TtfyPnKmHNkQAb1AKn26~0l@!4u~pD7;m zs(W~)UQznhbxPY%cHkPIEsk4j19!q#t&?d8W)xoa2m>_4|JK`YE}#9CUt5+}Ke;>$ zzwJO=D126b?b_&Q!OdN&duMqV97J_ZU8L38&`P$?QaeJ{X8ZjJP29`Bd$4u-_kX+ekg&DPOE?0|fzjEbT)_bFjl*CaklLAYluO@Yq6W}wZNqxE(Ar?u$wB>1T zo#2}bYiZ;*gi?JVZoA`e-g~-yro3==WbUBn;4<`=G8B_82%%m~Th#(OaDUo_v<<_~ zikjhSSw~evB309G$_8$0KOa84aO06>d?`7SDI;$Z{_;9 z6!P6)&rR6tmkUW~oE5a22@wl}v>(!Sl&~WB0FqDLa%g+3Km?>X*=lC!BTm}gac3f5 z4%5CO5k@icMTSJnss~2ujWN#~RS>|AcC)kPVdX}45r01Fn7Fu_geo6w)z=(CiBp8z z!uh5Ltknos9{ylU!X@mkXO}(H#~3(#6QFZlAjU-I;Mk;a_!MKm&6@0DA@daydMu^& zsKl7Fc4fnXPHBQ$#5j{;V+pA@-)n@YttRC7@mY)7H`=K`-Q#`&>;BcX z)df{~1OfZbh-yEk+GBC0A(0Z{I?8|*G7tJwp4kX4M&_Q`O|fiK47v?p)d!c_5B}CB zAbUy{A8lnNmT|0Nr->a04_2_U;2<$ldOa_He`jt(*7Edc%HpHtzhxZ|M2Fg2yAE2m z|LpO5%Rl_pFD^H4yqM7t{wT}(HT$PjqGsCn-M%o|Y7s;Ydovh-_H4lt-nd23oYCF^x~tgZ zjSdWozHF;()VI=fTwUFj*0F|ogEESi6jpl~Vve?C25{~47ux$ondmfn2m{eq7JvV^ zYKeS2aTx_B5QD2_o3WDHHQ7F4+oGS8VPs>4`&Ol$`|4B1%FaCH}Wh}&D3i--E zMFbpW_%UVnqvUgape%YO?I>bzWWe*janj{A1z!I6j=z=kTrV@=*o{*mzvSl}kYC%Z;oRF9>3h`p>W4$4ybJlZtL{eA{cmRlm zej0tF4XJjO1HgdYJ$w*%MgaB}nbn-gueM<>d+%4&5ZWN2p3Eds8ArHpMI<4YZbpDy zJ;XFpX4#DUrn2}ru@x(NNH8IR;3LcGCRF7qO-e~=yID?P)E4QpDE2I0I7Y1_SxG3d z{D`}ATuoY5bEH7%E2Fp7$+tUs>bJd9CIl~L;E-_jw)Sax!lOK3JeE%FGD>29>ij&b z+*oeFMzPGo6@4nn8e?4nwN;NUhsat5mE1}L4;D;)b%@Kv-g8$}hPjWzRr>DT{y`SB zN#=6ok{JY52;coyI6OOj1WLU&H$$=4&)$KnKA$|uVt)JWW$T4k${UT8nlc3&J1?@5 z)n&BA{Nj{UA4vEsPyfa?9Q-72yE-z1)L)MamqsRe{A~ zTM7HUD_3i0qsI0-x^nn_^RRo%!<{S3_1&i#8c&vo(eUGYx0b8#-Cf?Gm4<0f1 z=g%^Ll1kTG(fi=t$IHg8*O%8{d~JF4g-$;+K|g{6uMmyFBRCSQshLK*R#X$J1apch zx6iBzxyWOlWaYJB+C3}Ys7=iNvtYJTL~1iwl4TU<37$a{{o~5w!zV=1{Fv#8tY;$A z;-1O3A39IW9}(!mJ|_wmtGwFU4fhc^GmLh^pV98DS1Gi1w>pi>SN^EhQD_ubt<$kC zyvQvY>o?IGywE+muC6GcavN#YF58Q*7Q4nq?fRTD3Dqfx%%4pFRJ#LBnL>J!MC)%W%SSDgxhLI}EAu9Z&1U_rjGV~lY z^&c@2q6jBAE!}HG&JjIB2^vwi*9cj;l;vQ!PW7v|m6K&{Zo>!))NBfO)ClC@N~3~=>`~0Din2}{hb4pMas99` zYtA)g;dQT{BAQrg@85g39MKzr*N|`aswbp^zy1vK*CsoFraK0|c-rdhXAQk5vb_%Z z$P6!kQ8g{V+PT&w@Aci~>h-I@!Iet2cETK%AOThdgKtDv?@>#r8QLO9<(X&Q7%7ZjzJ3wjJOJKqq)-@qMh{~! zc^G|aM`@>z&P1sKx zYvf@aA#m6LCSQeijV)Z;kFto4+xy#Dk^F8VGZ3wdU;r};Vf?-cn_+qcT3OSmYBqgDNIOJKp#vW0R~q>mk*U?5qU>Q5dp2=Z{(0v=|3i^{?d6wJZUZ4@GJ|9 zqB7(ZnPVD{RgGz8zn#+B<7SR9j3^AFO$74aPaU<7P+^fzbO*&&o2>eCESt%<)he#j zfEO`Mv{Bagaa@PNxH=rVK?H+Eo$b?K-L2VrxYmDrq6;TYjM{RRpwj~rs76@?hl>xsib`8q`kxS}H^Bo; zDl5jH&8tGpc2ak5>t>OXSz5hQ(^zI|>_5d1W9oOLoCp)cb`L1<87*o{sSfRXvlPR7PEgfp?{umKji=K(zd-u99#QE(Oh}4QD?=LX6IVgfARji_wFq> z+W+RI+%u2bzTx57i*u05>o32&ywsVsSEA`3{((;}JMErX7nY}OnatqW;ffo0WI&+# z-t8tpxxp~k>=Ok7_C#v_0KNwY;l{-4v5`~l0|Uk=QG!-5|LhTu=YoyV559B2$4RHC z9Ts@79nl$84TQs~_H0?`R?3p%L_{_;(vT)vRuhbc%j$M#2TBGA{2m7%R{R=%TwO-C z3166BLoibs(FgYrYMMQ>#$w1|D&1vN7G58k9zLxNwQbN)Jv&Xz&Y?y4xVp3f-TKF- zuc997lX-@OJ>$aFy=Nn!2TJ`M=JL)bIJpRV-PA(&DG&ziJe1Bwc)8}QG~hD%N{0vC z6O8JM9|xG~3RaU=d5Bd348y6fpWnl!h0o<$+ZJ#y_KHTVLu`XYy0VbA zk{2kS71|tI43uDt*=@eGTUt>rTqXoAg!@Mb@mVuc>_MpM2#LTKB~rrv*VBk>LN|?j z6PSWjY=zLD$_*xzUL~y05?(1hY(c4Tt;}BIA$XmDq|_SPCxA~YZv>a&f9=?`Vfer= z#*JaE0zPRmmV=xPo96_gN+qoK>g#F5JS>zTrzCf>f(&P4q5fs@)b{llL`qROAHY%; zF+^p|MQ}%)Twd!=rWDvZI*6?+FNUt4_UAdd8@Yfe7Ydf8x}9*c+<$Oi zyCzNfNAZAlOnGK9Q~%b22F#jc6opb|)R;hg(g75mEg0TfM;opOKc!&_4%`M?@Lv%z z+HsS%$0k!SzKK%lTBNr)Z#MVKqei5UPTyYMOcC4<_ODm|l@?OHbmeJop50tK2l?_} zXyeOuH_xktdi6bRm&>!i}L%NTQe1j;6d{ zXz|BB{qfGJeXSF-612H@CU_wlWZ2Alj0}W1V1yz@FJmAi2$B!8KYPy@5?qQ;Q()Y8 zgfVC`Y-aj1xcOa<1Y-Tx1i0+^q^(S$nL8onsJX5E;J(>b5!Ox|g?E1Zb_I`qqLH4h zYh)M~;VJqO0mDeZ4t9M->qjPZqZ4U2|rFF<8ou`wrFW>Ux zap~)ypCex;VLv|Hr6texvJb^s!iT0m&4u9Lip^Xyv)T_3V*b;JcovNilJ&==QsqT=l6h)nOU@{0$1gv37fg+K_NA7=#^g_w6mSzt zJ>x$oP=&Qo2L9F3{_%+S7o2HhkdJU#m#ZJ$qlBhDl*g9Mm)DF)`Z1xPN@QJBUt~4? zn{zgMR&%52%B&yyNXVPi)Qg=arx55$G)nHC;;2nVBC|nT#CbWKXE`4SXG-y;K5i5X zK6`ryA4At5Ryz!P4QpfX5$IXi+7Uw;ofsY?YF4RqWkwU>MjNp>tBPU+4A_QPU@NxU{zE*m&?@xgUNuI=}SR&0^`>t@2u~o)>SuJd?nuZ8xyp z{LTK}jc8_jdC&r?`w8oNt(<*77s|bN?=Nq?e=j%AqmI;jy4-FnggYIH_-e-yzSg<7 zZ``}P+&jLveE-plxlpt)3e0@xn~ze%6ZnVskl^0hEP=b?)5o<_Q$-iSD*=Porl05} zrOV~w=?MJ5V#0MM%^4=aVQ7LNE0xpJHtO5#Oc5q9*T~BRab(Tj1?m`Q2+sa$_wFd@t)&maA`hv~9|Aub>Zq zJzrt3ZD!P$j%>r+5rtvsD6a$rt8b15!@RjUW`?XGBf*PKfg+o-e~FO@nYNzE7R)<{ zOS2o!hIp2ae+0AWgmrt^o>m_q5umgI0y>W|4Y@V%u*Qr}H(+mDyppA`5>CVw(W>1d z8wXHRP|8w^H`6Bs8B#o}LlGcBo(LpF2T6U)H%uJyC2$Bx{|G&c^N@!s1};@T?k2*L zQ0+HsjiQ)Hh=^pu7OQcrboH>9gNF%PFcRe;cKRegs0A(TJ7t5!I>%6-V=4}u%P5|i z$f{*F#bU^a_%WH;ugMEmcGxtjR%7Z;)@Es4bN*3vt}ATAQ^4z!asb z{qZ=eU0*_itBU9HTcdhEgxT{(k0Z0f_dK67pV45?r?Zu7B%m^#n6knpI#@Jnvz~U-Z_G2mSAOYh7h!He2?iPWM?&v7camYOczZ~MXYHTWqqM!yi7ZBj z7Z->FPB7uRFL-S#t*K|O1`D8AuV6!%uKWHc45bMU@=*eO>KMgvPw?1bg^w$SwH_?E zZtQTm#TPDFjIZq>qAOw70w!Ps!`X@=1qc_j7!A+#cD8FM?S%1RY*puS8deRy_J|RI zG>Vh2sx2rgEj&b^G0;Y?1d5HK#kefDQ6fDXTCXHlUjJ(bSBsAZCWyi-w;UJGW>GPl zXZfN#Ey^`(5~^dP_;i!7Wo?ozm6Kl_^Ubk*Tw~D-WoKhG^^J;2;MsOt8?TDNKWGB= z%Fc45@X5Vs#oW?ia(7&zfKYy6R;c8SJjvVNI^tfky!0jo2noDRm zL9$bOMfM!GG1|4tI=EdV&(2GMWOWOkj!4J8@N-M?^9opTU<@9+TH1Pfx= zKgu25?L;@tIP?+@So`MKN(_&bMrpv=#4k9`zB#!f2xSV^mIijR6o4QC$hwpX`wZt? ztCWc6D-EM=!kr3f_Ny{X#u^1!0J$17T#}J%0@Y+YK{cF1YgJ|p5nUUJin7b~R=bRJ zcEgz!wP=164P_CH&Mune6G0@rR_La!tmCBcTX{>zO4gRT-_@V;0PPAe@6oJpR}G6k zSHnU@JqHJWbY=`Czb~FEOS^qf*#x*xGHfdJyw50+ZlxQmURnMA;QD|3_~7Yxdh>jR zeXX#U_EC$NCMG_D+8#o@5H!6H6~Pn3w)pzPVWdc6@RP<#FRg zK$r;d#B6W1>XbkBC;}mb#_ySw($Ppjg=2{k@)LKPpL^Z@wS7_=(qjNZi9Z4R~!{hLEdWfy8jG|rFLpy-$;zg zRsRuqo7lUEl7f%bB3dmh$wWZt&qi%0*xb7|j1FR)bsK6pn66I$#(&u(lka#_zGOx; zpOS>{>0R$GgiVO+zY$D*85+k7-Gq}ZZiPar4iCevA$zzvDxB+JR?`H_@Ivs}wickb zO1oJeg2~2QTpd0}k??LifuS{UBS7oV)7~Gxf3|%4z0-EO+)LPBUv_R?U9McqQqI4A zoG;(@0?W~p<>29a%cFby%cGlDmurP%>epGO@q~_w7~>ww<#R1VU^l6M_4du>Be$*> ztwW&%&@A2Kc;Xvxy|{e-i@!Z*-~O{d`I!V1j0cB=Eahl^!<-hE1ow?mE&4P%;>NH^ zob@B%!- z1_4UAqA24HA#~w;C-|B`rDV9)D4bKiRM(D#@r`nVHTs-UHJp|<&Kmm6*64m6y@|@Q zAgxz%HANFmC9oYOFosv}$OEkmqm_x?!&|UX-pmWR>v`42mkhr;fmgMu4e6}f9Ys=I zU|ezWRc#HY=(EWl4Xq8~4sq0mZoKF!LGYTe9sKUyfIVXn@(pJQYI2BBuU+$g3RaeK zFTZ>5YG_~88IJwI(# zA(Ags#&p7+KRiGpequ^YJ-C|OVV#SXof!SzuGa$+5kv1e)~q<_Y4)fMqYa}13|KXN#^>G*cgr9x zIzd+YDjy}_)l6nqe-#eCCwifG!qq0^v-^o+QC6Q)j$4YgHOts9@(C|ZPAB}pBPNgN z5cK#UZ4(h8v&@Z}OuTZR*cFt|VSKPLUUykU zxoV6qO2eJQDj!A53Mo6m-h&ZVP8U1Q)LhF5K(%c?0c?(0?A+0HWJQ=!J;V)$1vN@Q z?<+G#HC2x|BQ^RQLD1ZHF2acO0-Ya}_){CGe$|xRgAW{2|mQTKPt%#o+%a`7M zXZh$$w}TZAwXKSy_0sEqB%{>ffjNLAxQsC3+Uj3eh^|Tt(1Ww+IUHF`MAVH%Yinou z`mMy7ST&a>Um=U=lB^cgu-&MGwYwXAYcBe^yzm_h9Gpy+Dh?0NJe7&ri~=K6A-hJM zta}#ry5<9R*MX%G2H7_**%5MFpF9AZw)#XC7-jV{?2#2THp1F zED-HCMqRYXD6kDQ9PefTkqHhMQ#n#KqBUu4VU^B|Qqrm93UeqV^w@o$=w__>OGPlk zJ7qxt$PYM(1+U0IKy9R%0`J9|ZeSTPNoN!VAo_&&)HUhAyY%&Km@e0>)mNqkBfcm?9{O~vI++Gxu4FmqV21H*8IR(s}l*^Ij5KK zPRWcisO;0M-!YzIfLREFM(V5m=P}Q6R*ZK-i)(`t+}ymGKm)U2f%yA(y8UdzHtjZO zVr#arjo^$-oEXL(q_gKKv~`tckKiLM!sRyBB-m!w#WLaSq73~TEk|$!hj5j{C{umO?NcWWxGsEj&MoDhBCrhYl$7XfuDo zmG-E)l6&S_D_>v8qW|Qlzi+vI<3~`j?6|}a`KDBRsaM*5iK@L1Ax<(MfW)VEL`+oB9;d1NhUK6@km#g*h zhhKj+Wzonoc%d~yhqVlzA!;mma6n6Q(pv)A=~U+}_%b(fnB`Iv?t(KpY^zjDg`bX6szlH<-^&jvVIz&QOL(eOK7-$ zFgW&*6n_%=7!5JNtdFSn>X7eQZZg7Nf@qA=?zQzW7GM#A=3uPmEz#65K?Kn>^*{H~ zJwwmDT~t@u<$zqyS}Z+?O8I;yYMjS*Gy}E&ujm@ zrv##f_0lGVfB5UU3;SoRD?*QjUPo-pN)Xx!#*BSE9N2{4VA$`{Taa{=^WBD8n+=0m zJ5X(LQ1}NN-74(TaA@qd*eYQPALu2kxy$_vJ?PswQ?{6>sZ}m71cO8hriQtZAZ1)Ar^f6 zr~J#6WSCx$6K1(Lsu!~kxv_ToI3mwTVl$4N4aE}F6vRn_?(u1du|(M0yB#<^rhXvI zth15a8~{!zkkYzX-w`=K{`3d*%WKpz6X4;=4vjNuTN?~rN2n9Z5x2Dl6H8PdXH-+i z@G&>jVg2H0a^@*EQ7%i3vb?2>@V)BSU;-=%x3a0CNfuJHL1Z@%;XoPKrtr8~>fX%OXQ&$X3M1mBA2yDs<6fiy^f zUmcC3`v4FOGaTygPLW63`<2sE?v$A;NXe`V>#C!|+)o5rG_vkS+9J4epw@{N!mDW( zX<6N~30dvnMy!#sQ$oG=nzK=kfCB&8aD1hus0S^~LMj|a7APB+mEW8R|NYFjM+0DD zq)3o^coH5>j^o-0E1F>})w(J@>mBhrTX1J!8n}be2!af+BdfK$C=-uoC`YLHL6EBh z%siTAnUL+?a}rcr32q7a?5AyYHlCe)Ixej;gUt-1rksm5D0BprZ}~N%d-8^i^kD=| z1N!=qm)1PT3!>T{q0WkZ&&C-cHLE8>tXL0nb&TPNmH;K}2z36@lg8)!#;mzia3$Vez^tD!Idqamt=FXjvQW}~dZ zC822~Vlhl)8$Ha}pn3~yEPuAhz(+e)myiK-8*UZHZB(;2!|_!J6TUiS;>p1lw&XQu zGQQAiT8>7xHifA5;>i0d9e-n=mp0PQcT74Xh zeK(+Fl_g{N= z`Nglizx-?e^1ry;Optu#YhPJjfAzHl`n9q0$2!bHKg(6Kk-zpv+Z^1yb~9yqygW^j zKWH)3J9qCdU-)`kD_8!@g<9_KK3JX<+R3f6fAi+@H0%C;arjqK7}pBFgal%`o1sIa zBm2aF=PHH?MM5BZ*G+x5e&v~U z1<|asj3?X^dgB^Ac`|7kVJ3!I#5{uEf5 zF<|XE&Eo2bN}wbh*6s@&4IB`-in2D&o6w72>Ql-jIIuE>hMkJosr?o{%^?v%!pTf5 za74(xv(YhH=O?L~EOScAd71}?@0B+V zHUCgO2~_hd6E<4;&ZEUh)?ZE>HAaqr(Wsv0Nf{V#I?)&XVvT{BJbbGcYu5t>4>+c)T`Bz_)4M<%?rT`q7=hEMu~0^>v54!m(c@;n zj{>e0v>0E!JfT78*;AftVNA^E08`GE(ZOn~$da90DbJ?7=qtbcD+%d8{D*$m^4EX% zZ!JIjH~;?fdw=4G+6!kDTGwq&93Z&mc2h1pELYG?>DFt-`illBmI?jr zIkyUY!i7U-j2tbPn&0v@!lGvEN54Y52G$lMtXem8K4P}~>*zh+Dy1Ice``Z9s?_My z$a2>+Q<+hR(HWzve37)w$deW`*IEBx}F*&Eio4+9w7ty2_XxZ99*q8!abb{J7INk@hmK<#avk?LKzOe!IL#YVA}C>;)Q!= zpO*L_NHrR&T?~uy-*Qo?Hzt@-4T|r-$=}nW6ULF2<-&(56mcsh@`1Hfe}wXho!-gy zfiaCrxXf7e1n=xb+HZBZ7w)#5&}W_P+XFs$V76I2QyS^`jW6d~jB3V*iYHnkBvNJ$ zVyUl$8SzdT4}%Xs=zHx-c^u_>IL{R#8s*e#*Mz(+cg3`e(l|=Mp9hFJAiTUb&9f|D zS$y`D)We7YNZQD3KZe{7FLt)v$#?EL zvQ6Z+2yv!@Mp)cl*RJ1g*UKAC+&)?EK6tu(;frr9pZLrtmQVl0?_2I% zzqoS%Q`Vh0N141KR&5y*Uc%b{sRUPK#quP6E=u{P8v;!Y~#us zS6ux%>NrFz#Lu}UQefI?M8~bo&1kCubTkIW@Jxa=KAZIm@u^M32zIMg(d=<>rHuW9 zOYIQlGcAg)EL5a$8A2Mk8xGGRdd()N<}13Hd4_0wBfi!qqeJ|RR;EqWSB902YR9Ui zLQb&cCK8fqM204&B^gJ)g+g1f2wp^eMGt*(B^lk5o%n0!d$PLsyWfdlcn>1^b)@jr zxdVE3d(F_DdixWgR{3D`J%v}x+9{d)RNg288o4%rY|N^JE-;a%PjH{~1leevz7-_D z{!Jgd9|c-|k1wAqbIMKjOnUD=+%?61xUB#GxpZMJ)?E$9Xp!{v5!!4FW)7?z^6Xx^Mtd7m$L-TahB|9g6gmptcMBCd;e_FA=p&fe-~5WzvyBMg5I65cGqZP6z_zGH zbj*G}!%Zt5i?QEI(DU)~d)o-^ur_ZWTv`9fA(`N+B#l7_Uh|J=LEN4D$EkB#;=}0+-PE)LgfC?1J-pt zgT~}gd9v~+^tTrx(#9Y`d%?Xi&L}xjZxnu`zny5(C>wp**WCo>S)a{;eZsq3nG0bc zx~;rXGxUnI&$%>7-Gr-7%w#@U^-WkKnrZ_HZ&v;|e15P{1tO3+(!^&eiEr zs1Nsght9cJ7+SVOKvrmp3u-lhqE%&1iK7Cd`P!*HLwDuJPYG7^hxfU8g?Q>OLAO~H z$VPCzEFaxbl;_DCkR2m{Fd8WLMdpkQiQcp}8p$$RKa#oX^L$7l9r8!xr={MxRnF@A zoCm>cu6gH=OXiERY8P0~MNlYD8Nq$MF7sUc{0_e^bAN})zw>0%?~;oInMuAL#F7bi z78+rV1ayQbU(sSjP){?JwS4%^{2zzDKc0`3tjS3Kr zd_wyyhi83l7WciAA9OupQ6>6F29>80?Vf1j75SQ`6(JF6_KAHG{z#8HBKQVL^~2UL zt~FL2>{j@#Dy8-`96ib65Y%Xzl+rS1VkC2Hm{DiP>aTRnZs$Y?Xz*{^o?vapWv{oo%S?%2+1}fKN({Zn43kpUW=g=URQuxhVuR9-u4ZjZ zLYIL?QJFk<^rN5$mf^sK+ZKmJwu=Lg0wlGG@(cM*v@t)Vi06VKS`K6OEMHj3;;BNUHJ!D3;;y5i1Vo2%0Z9op%e`X z6Gqv=^AFlh@txqQ`wpz=9WpDS51kSi?D#IFrSwdc0dlhb3xwTW2PW?!_OR_0I^(@OVA^qx7|$RYk~Hup?&wywQ_`C6M*Z%V>Uh9bOnF)^r1T zvDu}KmMPaF zVHvSh##R%<1Ra6FU5X2$q1w92OYqLhPub51mJ1H;HnJoBM?i+7ldS&p6f8QVl$lmS z9MKiog9ofZ*=+UGIwux>$BzgmW$TYgTG2OzG+}6?JBq>vYUbkBaRz!q4JqldV8deF zU=GeI^y(8Htk2v^DnG6sa+&ddksxmT~d(JN$||p3kM}t`VelY7PUb1E)l@1ZD{<`reLRi4({z2FT&TO}e7%E$cCt|RbvV}O%JDWa*Y?Wrq#p^L-=}klq zGxW}hOPK2nJOW8^K~A(LU(uv2QN$J`i^R_#l$N5IAi-~a+4V5tF=yzOcTS=z{p6`{GrWe8p2fMfHv|U+arL8=f-NO; z-h6`J=g|Ud(pm+p8x84A(A)k1E}XXtk2h}JUS0@qMs6CknG)LzFGhs3TAP4lJHlHq zH=^Qh+6@=Gg-h*soZt&L+Ot0M_Ph{XJ9(ZyyGJxO8GE+e==tW(t=WS4afihaaQxtV zmA@yDpt8WrA~I*6`pqAF{p$Ym2Y%P5mM{JGo1IVctVLVxqZ53tm1e)XO={-}THbrs zj;1NI%}n3pg#WA{t~awDA;69>uD&`Q?!y(~pLLI3@sEuKZ73$jKScIPZpB-N3ICad zl&6l*d~dj770-H?Om4VjyhOV7E5X;ZvtYShTi}T@U>H~{VYN8;;ZrW7Ir~=q@r#gX zZa(gxiDWB{g-Rm>D5vU~Q>gmZW)rYp)|JYQ#`@NJ{CJ#Iz1h|iLW|Lu$zME_X+8>@ z9AH>jr&G4zUE796DlhsmVhw814>f}SsxO+^Ex*&?2!1P-cZ~QO^`19Ueeg88N-m^I zbg*iJ<2GxZ5bC2Ngns3&pT5@G`_hU&lcw7btbBd+&Q(6|$AY_j=DB(%FeFAyPM)cqjVuK5zaFjL6KHsZ8CuIpW6dQb)^|yVJ`=+`baIw08!a*=C z%(#hIf+uBE83ZJSQt1PJ=AnZa#tEaIge8GhF_lLMM@+DK1}6A}(RmiL5ru_flRj(q5PYk0qFl0?S)H?A z55di?FoHYA%-SSC!p_>yy6RW#KQI6g7QTsl!)d%F6mzubuosyJAHTg)~ScGYjx zwVYpFh1^j>vLt&U?%MVJwl`*X*3~|~_bL!NL~LldNEGgwIkha|u$|Bqp6jHkD@`bK z0||?~dE;j5Vc3A39A&~b|N9$nzSEAH_w&Oy^15;>p?JU8{6>QL?04F`-hS`a7i=ZH zQSEjC0yTPh>4kjyopANI;|EV!>%pA>+HF$TLMHpv-+uANa`Tm!5_Y*1%6phlnXP|I zKj$h3prPTRCWg@}Y~kyubp^H$p1G1rPTCEu0WoX_tN1Hg8{UCGlpIiItx5YB4ZV0a zMiOPXLhw8T!Va3_E{Zm7%S=eKzJ)OgBW9wkE8q#q@Iq;un42o}p?brEW*KNz*ot>N zUn{|9M0|K!x3!KwQ)0HSz)LkCdZ~;E&`6dsGpAjh;!hD@q(eMHAeJ87@wLfr3dq(e zWGh8DZru16O&G!Q5Xh~XQG!pOgp)>D1pBnB7o&dgmQ(t|a_ZN@X%#uiN5m$A54_7UCz8C!4wb4quxH@EH0p(Rb zKD%BYxGh3t2qrKG@BwR=Yh>V*z)Z-U7n6Ku2}ipV^5c!*46$4k;;$u!JOX|!#~5X^ zrF>xI;AC<9XZJ1HR3t_04mBO`9!TlzjYE1%vExV%Oln|~zGxb&ZiK(88h{->V zp4z36i^c>sKm1IBr95NFm}Lh1dbkSi?w0mRAt27Olo@&4Bd2k}3|S+%%f(T1U<5%Q ztgL1&NA9RqkhP&eIH?0W#bPHIZDBAluRweDi21e@{iJprWvLExP$(jOlru1^;JNBg zj6ndU<5N@=L470c0>CKZ7!_L33q82C*Kfk7w$C;R^~FLfTRdNDLiP82`XkF<{oL1< zR}Pw7%vuA5-HeK{glhq1K6@_3htd*XOG*RP~^3861oP7E$jAC?iuNFpA5l0bhui;y$uJ(|Mza zjVy5n2#TpImCi-dD70-TjCSoS^E9D+RGQf z4#z|9!FbM&098sMSfag+aIh0Ca}S}- zxo1*rtH>2r{)9<|n01I4h(d&wltQcU$Wb$s_9merF@n6d&fSdx2u(xXnT)L>Bt6@$ z4sar}(hI5FPw*ISQrss=Uj9Opx9g0*`a~2YVv`Jn@M#BN7&+tef*9`F(*O9A{y%~rZw?2g0nI(!LgA~?Ir;E(nnZT zhS1E@;MQ`^e8j9PWd_q=20m8TB5o6|s}K)VeD44YY}#EPxO5D)+nl10-}Sk&DBsh~ zCYD7h#k5a4eB(*`#T>EV)UjD~NlW`T6L{!OQ~)=Z3AS~o4!J4_`s+SL@Eqhu(bm0u)Ym#ocE1hI zHutYDZ@l|-`Q=~xMhl=iQ?`Y02f2A(X%G5OeDsy&wf3^P+VO&WEsEOCqTgx4c=y`v zWuxy*{#+%&t~h$e&b1rYYj3oPhWc%#?Li7=zddy}a`~-i7ZAl@4HRII5{%v{VsNtM$Vqr^tW|lL0R&*}J`6Tay29nmAbfmE zx!Gr_=b?Sp%1!qIG=s^?It!~fnYeIv^9&euWPX+hNI8&7I~xIBh{M|I*?hoXf#k_N z>uY|OMgf=4Ar>qFDmotE0BHT}flPgs2LmZe+oY?P^ZtW+y!*$lQ7#wh{;^lz&Cgo* zJg*pwFEeYI&zSpSU;-AY(eoJMtfA;x79~F|D6q`U3Gw|o-}6cEIl@dmH-WpGkQK=> z_>XpGGysMH*<>8+b_lQh1f=07P0a5YOHUmW@l!b_Ys8TY`O37ZJ_Ja4NLjm@pfTcr zR6al!>=`>lo7ck@R$_(QL{?8@)*>bhKyls)-)h2m_Oq$%O)j1GdYL(eu|kXo zbfoHP!`jl?Twm7^ON{DuE~6y4CGC8k1c-=`bH8I)4p0Tquo!xf7%*x-*eG@e&?5{7^Uw$nY(BtLw-P)eAJkB3{ z@9j62+pn~z&JByG?gf2Lj}8W@-baxpE1yEB^9el+T{q`v@T-rpND3KJB{;p1X}h%(0IvH3WBlZU0pHO9*BvrEOpn6nH;U}6!S?2+({>N8L^Ct zk6*iutNg|d+DFBQH~2QspeP;i&hHd=e|z4T6`b_PoX&i&@6CNp_-1=WCGktMTKzMR zP@4wWOToX;QLu8@|7hE z=-kuf6tu(GL8m2us$(i-fQ!oH;@;3UN%JX&rws3rP8cEY?^q zJT%QiFzT(%@vk?Pr$Fc-P7B1f9(hK{gy-s?5U~Iak1P?Je8dmW&f5?;a!?(;Z;=%7 z5c1{-+U(7&%09a<9MJg0epBl5d>9esGE%%}V{udtgR8~jrk#N-g6V@Pc!UT(Bj_5- z_<^e!g9=^E2G5$Vj!bAu$cCk@7CkLa^NH5g7%qiaAJ!-)P>r1u&{eL7S-yoKia;_p z0*JzE(JZ@Y7>=81o5^0A7SN?J7$^q&>jW+zJp7L}Um3G7vWnle;=wt>HkOp>n%W_} zrN}V}{H>dGeD_|$;3{`g@SerB7<~Njl_OO6d*66?XZfq&IbQzZ&%Vf&bGLPqD6BOV zYh`PUHcUiIb>b%LoiHK)=e*zuV7E1hJ4exB2h8s0dSTse64uwHLO2QzPuqBd7tc1I zFP}d-DgXTa-nm)6_2%{R@C|1P*yxpF441nlJKJk!aKF&Yn_}-D7qa>M=~v79?|-=L zKjMkE%TSXUF$h{o}f zx=Zpsigk_$?R~g#rm)WjSR3^zp*uNg7*Gku)fH1);c=9F<#7!T#ds5OhqTra^iFg< zbkH;7)=fs4e%DGb#D@fB?bYtY>esh%@l- zT(3m5XRkUoPk-fqUe)v~zxZ$VfRbBtNCgvI2|LTXQslUy+~oKqP)6S7QncC+@|Q1SjFmerFx$tHN|B?lhcGe zrX>V1^Tq-zSk$d1y8}0|?U(h>yLG4bF%hBK`~9a|TJCXmV)mM?O^Rst4A5}FVGa(@ zmroGs^AS-du@NvI|CbZ}^<7VSBw^t@Rh_n0tM zissVHZab?!ZFG8C2X#gywiX5`>Zx%8W+VIFCCoJWV zVU&nqvm2S~rA!Dz8w>z5@x6pHLj^~{B|5a{B0#s89Fd?y@5UfDS&7-lk-FOpbu;Wo zBhB{)*Xt`4>Wj!IGL9k^RAGt)zxIC+Kz3RhUHBgu9=z95% znO%xDs~({~?|Hv_20B;I2=Dr|`ZV2~0{te#rh-1T5%4ua9=RIt%reHG|Ef>xqi^!B z5x$md9cp551W`h#0q}bxqSeYS4Kcts4TC5e-0Aj+V}(nlvVMCjp_8x-5M$MFh48tG z2A(JmTkvG!4c4Vo@Z5Wf3407#l0M_s%2kmUu=f)cgn{ zE(i9^EMBn;s^gc7iIu$wY2+FLMW_}|E$R}q6>iK4lZ4=|@W%YD+%yC*pQ7n0v5VoH zy0eodE2~YT=vAT~`f*p&;mmOs?zmPW{@muRA0vpM9UM$+S&BBSX_G}gM;d||rtV#e zkx<5ZiMig3fy2w>dJk8Gq&!3DR#c1xc4V8)U>3TW=BEB*fmfcpfF9b`C%{2^jLD$ zbF+1S`S$mIu)P29N2L#T(p3G=zjvNe2Ll!}rg!ZIL+cCDR!BX3)@f356kR0rFAArX z_x|*xqXs)L{IE>}$4A%88~4k1^SHBE+c5Vqn4E#}%V*2){c#GR5KByX^5o_6`Il|T zY8~M7$1j%O|D&HR?-tGTJHPW+a}6Do^z227q0E>p|HiXvT_a4cut-s&bPkU*ybyh` zhKD5IP#;)>yC6pY-g4~&?JE=yL-M!j$=x@z5FUTgLF01wos zQ+=$QzJqrZF|#qzY&QDoKW~!ID>6k*3sImIjOD|CI?Ps)E>pHeR?kl&i5e_5! zY&`QvKm(fhr~Iq;x(A@ii2k`Pds{DftJ6Q>bN=f8FyUaP=b~|@Zsoq&_1c8}+MSSU ziWmXyWL1wf9zi(Fcst9$LRqlS2Z^a!qX8)Ej;n;_INPuD^N!Go6=sKAM2~2Zl>ddV z$u++2lFX`4n3{TG5LqyD0`wm%20KM0QBLd~>x+cV__-697|79a zTUj>zwB(Xl|82@FqQ3|>V+E_2@4Etto3Y$3>x-P=1XyAydMjF|_bmnQr@ZQUN`Xbq zYN^lp!Fl~DjM>yx{l;uDbdIKDwWoAvcVl?j9hx91^eZM#axEf=F3 zlKU3^-hKP6j{e(U{@}^y%Uear9JTXReN&bG23JQeUIgP&u!K#5{v>PsxG==a@N?RN z=Xo2c&I<@Ak~5qpOzY=PmivBI{+XFV8kV(9F}`gYPbH zCxq?v{_FqFpIyHH{U0p%-+b6&_sOz(UW86XY)NXNWW-?xIXZgOc74hPXiotZrS zN$l{o3N-etbioJcsTV3u14pUNW3fVgBdaD&N6@_%rrL9V$Ocv8BXn)=#PEXARo75S z^uU;)a?BQ6TAl^bSF}$tjn9AHto|A!s^;xFgw8}7F!74YWjWdheVAO1mTT{`ZkqCQ{!_4SuO+zSR%hCDNk z7=8&37-HVNtwfuz>h$23?ZR%qgX7feV_x+6&9B!i?CV%UD;qmt60#vWq*=rRx&hVEH~Cv-8h7WfHSYAnpX{wzeP zezwqSCJBIOw8zykL|L5#EC0PPwqc5dl?_UW7xhfmODp~{Vm3^7t2b+A{GzpKa}vQ1 zp^Tua?83+v3|ZI_8>^!J?3AdAD+>{eL|`>z9^st&#VRJrSTbyMb{K-$&O+U4pQ5tcQu+mBH!MWV z2QMCazE68#QT-}50t`F?q+gUN7!m%)TNDK*KYnm$`Ro7Tzn4qpppeGv<%`k`!>O4K z0dH~tCTm|dQ$l99Mdgb&FX0w328+UB#rNNKNJ(by@3=*vu0T|}NS%;vw`Ub~gGaLza~52&@u zM_XtekM!T*Gn`5sc*+2)P6h;IO&Sudr8S%o#qG21M(%s{K$S&r^lp}o@65DC+C(iW zpz4VpEUudY;7Rl}t`YoO-_7EM%IK#VHr%teZ6ag!_iYrBTt@;V^xdp|GGJEyRmNeY zo;Zo$gPnB?;#wr23X8Fb?4a#o66f@LeuYK3NgKqZ2_}B&JNHKuvJl8udG(cl3KtN z5~G-sV;*6SELT>Y*@5==D|=Rd3A7>l8Gr9K7Jm2f%{FE!pVLM4#M!7l$@O9}l8eL? zc#X>7zYd1M8q?lfHY?Ka%D{nu$KIGV63?Y4m{2uJF6XUOx7V|Po?_16B=ptA zRl-Xo&_zkVdcAtX=>dxhY+3l2+0huzsQYVZoDbB z`UbwnslG&VzgMQR>aZi;zI!r1QqsTmXTG=mnIC?C`Nf}nw)_wO^Z(j2jGsUaKQnvj zT?&3e{OW7=_PC}P2cJEDwtU>--_}nggS#jb;wCHK#-6PN_DwPMcgjbOK*1Fzzz%(f z`2ZQuG)sHgA?%6DnWgmR(ELGg2;vhmiFSqfvXm^qn-R!$qHenf2A7 z+JMW(5gZw3X1wFt3Gj0go7qM zFZ9+mf~MNnM*CEtKlj0je9?NanfL44lvN+Y2<%5eAn?i{bakj^w1D^L$NGC^sBRhX zRHopsb-gxWzjiz1M;yYv35YdE24b;Bp4E+^SxGa!%reNL=f4Bbbigoa7#txnzP}JU zku(w4tN-%Q_I{Q~?w$?n!U9eHP;vAQ5+6iFa62uuva0RlM>ZBKlA zg26_gHF5<(>1#gk+8}&bERJu(yrOe15}2p`5q3@~;xoiftACdzgn$U_x)CAGIjxo0 zPcWDPtcR(a=2mx%DUCD>ZbB)mhbt_5f9i%OLRRKikwo=kgau2QkR?Pntv_6Jdec?H z&x+lw4H#-;k~$&Q?!6XUEwUXJw@zSYK{vGSQv!jkE}QYW)8JPjHN@Ss`Z-~pDMZmj zW;fhdqtK>|I>JXV(m&<|_spA*9whvOmmQ+bWJX9;&cZ9KSg<5Y(VT=`56Yv)_3;N~ zvwYl6*SjgD%W(NvTx8{1PcLOcG=_$nksW3k{)*P*Q6uW?1FuM@;MhGIitOX4iR#TA)R*U#V+D^mODRST{jI{eV#9-j zqsme&loLpYsF7Qu*{ZDlho_A{AmKSGXFV_}8VD~`5*Bu8+C``bOdDc$V_;UpWkhB$1Q&lB z|Mg7KA_l|bQ^<<`-jZDr@mH>nOyVXAp#KK;^_8H4Pp%`5@;Ux&uc(w< z3lbGyMySY=IC2T+j4~^qm71`hjWFSPk4q=BhOgXW({`2|7+kk5FvQtdR!gh<+Un-O zBZZ@f+fCzfSbao23F&{Yk*2fX(uRm4dKc!G>bybCjP}-uTZ>&Sn$Zx`;@m z;-`9VHj!~~5PaTv@NlwQ{^+Zx%e%Q>p5#Mp1Va-&6yI=q)l$R#+ulutQjIGYBy1DU7+gaLn3I+Wc2B>nZvWzQn+a* zB5Z1^AU;LR(Z>j3_cda4a4@4(&OKN4qHcW-e^b~e#_>qO+MW80Q&|tD&ik%U@;^TVk4ypUK5z%B4!s}c$OsF${k~7!FTEXBE=a- zMLiIYjy(jn)p|DFJI@l`Z?>`5;nc7pl*~bVm))DJgCTn-Vjo1pZR0~Iu}RESpZTiu zTO(-W%QR{1ksQ25yz;aoCR0($Vit5GKam8ku}&KMtZAg}S?tbjCEXp*hheVj%SDn} zWKO$?YT5K1k<_gO&79(dfVz=Gt3H8E%+Pxn+Khy6)OrHTiR;Lv47-Tsl?IBwSj*s5 zbVEQqA^}>u3lja8Qj#z$TnPE(2gPgzEB6XZxP8k#$L2Me+Xs_TAi^Wp{SW#M`o>p1 z-G6m?r#s=1dIbY+pPo0)u@dR$7~Bjw>PR4SHAEE_^y}9<@4mf!_J==q;Vtb`Vc=^;0)#EZ$Ue96Rg=7I;&x25|JV!W6xF)ClQHO@yFQoEL!sN}v zH%njK7~Xwv4imSN_N29f&!3$wKmGXQ<$2{k=m^3i>4=*hWM+3zMvLfXJGq|D6Ih~y z_}Cdl7ma^Q?0n-{U3+myLVkSY-OXA_NY?viQZPjjS)8^G;mlQndecU|i}9JSc)W~8 zZX9%?50#aMfJ;Zp#*@?5HDR2is+?sa7&;EjNsOXKI#L36oHRCMl(Uy zUKAG}JZpV*o%N)0js>Jp7#T_#uUVszY09oI-8GwrtJ-R8?mz^z<5x44eKc8HjJ+Mo z4OLZ#4Taic)JO-RmC5f!Hq>9n)Y9y7zepVPeR^(v=3@EuX@dQ(-s7Jdf}6SeJ#5iG zK7T31%@Zo8tdST@IBZ~j=~-h+%dOAgNZDXX_SKQw-^zAn5Y7AQ@@U@e(}1VF`Y;db zz&!i)#PzTDc3t`Zx&}@?v@4GQ*<{VJzAT9D)9#I3mP*OW)}&p?7a$vTNV6$uH=MfN`p5g@ zIukZ3=MM)$aU-oOT(#*-zWoTT*!F|vy${}4{^{@kL5dmvgH7WYYqMF*_Ki%Y!C-U0 z=ZCHDJV=V9B+s9>rbao$@b$S8>E&=!6wz6k5l@P0`jfpE%Sl4_s@VIdFD{mEf9L(8 z%gUuAN~bX&+8%Icm~>EH(O?8`ku^?8h|luu7#Iu$^tscRqloHL zSM)o0Y;@`krj!pJAT%jPsANG`Rrm}Jm7Ab|ep(=|TjQRM*(#UI%G!=7F_;bho-1!q zW}14o?yH~_D3PA^4Si7nHCNf0LpPT29ef#Qvp!Y-rJk^^F>aU`ixLval!aKoSLJO} z;B^WuCW|hrGqPNR02nQ$qzF>mZFD;O!A z((ZE8GXf?$B;vOtZzNV9!o2ux$6l-r1U~uXBW6~C=mi8EtIlQ|HUb=*md=83ZP|D+ z8?K@NzQ73uo5558AsiBBfv9H$7Vq`Alx8tkCt|7@V$T~{BD(R$K-Iy=YP<+?xp~pM zJ!(uVnG?6Y=w5_n24I19LdmkOY?+tT&EnWTi=e(-G@c$M>Jv28X<9AE4uY9*P{P0S zM{jn@*W&g(0`K5beT1w9+`TMJG5X{-2BQqNi~6xRJt-ujW-Mh8A}~ym%~mY95r}XE z&&IQkue;%;lJke@guqH$K-SA#x=F#oKYE~;S%@~o>36-)Sh5~c)Y1r``bsF(-%+wE zN%2m|W^YeYFFxLI&;Nc@9-nv1A^&;bzZ*k}+%dbsuiB(QS(VRw{`~Z5nKKiXY52=% zaBHmE>Q88`0?Pds_|hX>Y}P!U<=ek3&i-Wwo*X_nS$4A!^ARlfatZNkQ$+{C=26*C zKYdyfzY@s3^B~(cAHCV1XROY>X1I3ZQpTdHC`I(ipn)sxaaC}KAfsepzk^@Rp{J^; zp86T?2v*c%(R<`lV+^g`=r1EdxF^GaTa-a`RrC>*Qry-vqO|&MCRjhhrv-j)$LnUQ zW+Zm$3P1=0{TCn1%L&cinTl%1d$Z0Gp3rAlT}wy*Xk`o%{jVOhVm2t?fw6>-33#&u zX5Ts*t!1qA=FoNnlgnr3x3#e?KmyT5S9)PCz}*-atG^y{ca_sW0|4YH#Z42ZTloYx -{1qxU+Z<+m>ei#^Abj3oe9xGfa811 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

R?v6;Ly(T-NqXm+PuZ=b;(qe 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 Date: Sat, 21 May 2022 10:18:46 -0400 Subject: [PATCH 09/11] gofmt project --- README.md | 2 ++ device/device.go | 12 +++++------- device/device_config.go | 10 +++++----- device/doc.go | 2 +- device/list.go | 4 ++-- device/list_test.go | 2 +- examples/capture1/capture1.go | 9 ++++----- examples/cgo_types/cgo_capture.go | 6 +++--- examples/device_info/devinfo.go | 1 - examples/format/devfmt.go | 4 ++-- v4l2/capability.go | 22 +++++++++++----------- v4l2/controls.go | 6 +++--- v4l2/controls_mpeg2.go | 8 ++++---- v4l2/dimension.go | 3 +-- v4l2/format_frameintervals.go | 2 +- v4l2/format_framesizes.go | 8 ++++---- v4l2/stream_param.go | 6 +++--- v4l2/streaming.go | 2 +- v4l2/types.go | 2 +- v4l2/version.go | 12 ++++++------ v4l2/video_info.go | 2 +- 21 files changed, 61 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index c0f90bd..4b40e0a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Go Report Card](https://goreportcard.com/badge/github.com/vladimirvivien/go4vl)](https://goreportcard.com/report/github.com/vladimirvivien/go4vl) + # go4vl A Go library for the `Video for Linux 2` (v4l2) user API. diff --git a/device/device.go b/device/device.go index d475909..bf07dc6 100644 --- a/device/device.go +++ b/device/device.go @@ -21,7 +21,7 @@ type Device struct { buffers [][]byte requestedBuf v4l2.RequestBuffers streaming bool - output chan []byte + output chan []byte } // Open creates opens the underlying device at specified path for streaming. @@ -108,7 +108,6 @@ func Open(path string, options ...Option) (*Device, error) { } } - return dev, nil } @@ -321,7 +320,7 @@ func (d *Device) GetMediaInfo() (v4l2.MediaDeviceInfo, error) { return v4l2.GetMediaDeviceInfo(d.fd) } -func (d *Device) Start(ctx context.Context) error { +func (d *Device) Start(ctx context.Context) error { if ctx.Err() != nil { return ctx.Err() } @@ -383,7 +382,6 @@ func (d *Device) startStreamLoop(ctx context.Context) error { return fmt.Errorf("stream loop: stream on: %w", err) } - go func() { defer close(d.output) @@ -396,12 +394,12 @@ func (d *Device) startStreamLoop(ctx context.Context) error { // handle stream capture (read from driver) case <-v4l2.WaitForRead(d): //TODO add better error-handling, for now just panic - buff, err := v4l2.CaptureBuffer(fd, ioMemType, bufType) + buff, err := v4l2.CaptureBuffer(fd, ioMemType, bufType) if err != nil { panic(fmt.Errorf("stream loop: capture buffer: %s", err).Error()) } - d.output <-d.Buffers()[buff.Index][:buff.BytesUsed] + d.output <- d.Buffers()[buff.Index][:buff.BytesUsed] case <-ctx.Done(): d.Stop() @@ -411,4 +409,4 @@ func (d *Device) startStreamLoop(ctx context.Context) error { }() return nil -} \ No newline at end of file +} diff --git a/device/device_config.go b/device/device_config.go index c5dae8b..2f05bb1 100644 --- a/device/device_config.go +++ b/device/device_config.go @@ -5,11 +5,11 @@ import ( ) type config struct { - ioType v4l2.IOType + ioType v4l2.IOType pixFormat v4l2.PixFormat - bufSize uint32 - fps uint32 - bufType uint32 + bufSize uint32 + fps uint32 + bufType uint32 } type Option func(*config) @@ -48,4 +48,4 @@ func WithVideoOutputEnabled() Option { return func(o *config) { o.bufType = v4l2.BufTypeVideoOutput } -} \ No newline at end of file +} diff --git a/device/doc.go b/device/doc.go index 51e144d..eb92125 100644 --- a/device/doc.go +++ b/device/doc.go @@ -1,2 +1,2 @@ // Package device provides a device abstraction that supports video streaming. -package device \ No newline at end of file +package device diff --git a/device/list.go b/device/list.go index 702f938..97824f2 100644 --- a/device/list.go +++ b/device/list.go @@ -52,5 +52,5 @@ func GetAllDevicePaths() ([]string, error) { result = append(result, dev) } } - return result, nil -} \ No newline at end of file + return result, nil +} diff --git a/device/list_test.go b/device/list_test.go index 5a6035d..5f480bd 100644 --- a/device/list_test.go +++ b/device/list_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestList(t *testing.T){ +func TestList(t *testing.T) { devices, err := GetAllDevicePaths() if err != nil { t.Error(err) diff --git a/examples/capture1/capture1.go b/examples/capture1/capture1.go index 9ed8322..b290fb6 100644 --- a/examples/capture1/capture1.go +++ b/examples/capture1/capture1.go @@ -31,7 +31,7 @@ func main() { // helper function to search for format descriptions findPreferredFmt := func(fmts []v4l2.FormatDescription, pixEncoding v4l2.FourCCType) *v4l2.FormatDescription { for _, desc := range fmts { - if desc.PixelFormat == pixEncoding{ + if desc.PixelFormat == pixEncoding { return &desc } } @@ -40,14 +40,14 @@ func main() { // get supported format descriptions fmtDescs, err := device.GetFormatDescriptions() - if err != nil{ + if err != nil { log.Fatal("failed to get format desc:", err) } // search for preferred formats preferredFmts := []v4l2.FourCCType{v4l2.PixelFmtMPEG, v4l2.PixelFmtMJPEG, v4l2.PixelFmtJPEG, v4l2.PixelFmtYUYV} var fmtDesc *v4l2.FormatDescription - for _, preferredFmt := range preferredFmts{ + for _, preferredFmt := range preferredFmts { fmtDesc = findPreferredFmt(fmtDescs, preferredFmt) if fmtDesc != nil { break @@ -60,7 +60,7 @@ func main() { } log.Printf("Found preferred fmt: %s", fmtDesc) frameSizes, err := v4l2.GetFormatFrameSizes(device.Fd(), fmtDesc.PixelFormat) - if err!=nil{ + if err != nil { log.Fatalf("failed to get framesize info: %s", err) } @@ -100,7 +100,6 @@ func main() { log.Fatalf("failed to stream: %s", err) } - // process frames from capture channel totalFrames := 10 count := 0 diff --git a/examples/cgo_types/cgo_capture.go b/examples/cgo_types/cgo_capture.go index 63be537..819b20d 100644 --- a/examples/cgo_types/cgo_capture.go +++ b/examples/cgo_types/cgo_capture.go @@ -78,7 +78,7 @@ func setFormat(fd uintptr, pixFmt PixFormat) error { return nil } -func getFormat(fd uintptr) (PixFormat, error){ +func getFormat(fd uintptr) (PixFormat, error) { var v4l2Fmt C.struct_v4l2_format v4l2Fmt._type = C.uint(BufTypeVideoCapture) @@ -88,7 +88,7 @@ func getFormat(fd uintptr) (PixFormat, error){ } var pixFmt PixFormat - *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt))= *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) + *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&pixFmt)) = *(*C.struct_v4l2_pix_format)(unsafe.Pointer(&v4l2Fmt.fmt[0])) return pixFmt, nil @@ -99,7 +99,7 @@ func getFormat(fd uintptr) (PixFormat, error){ // Memory buffer types // https://elixir.bootlin.com/linux/v5.13-rc6/source/include/uapi/linux/videodev2.h#L188 const ( - StreamMemoryTypeMMAP uint32 = C.V4L2_MEMORY_MMAP + StreamMemoryTypeMMAP uint32 = C.V4L2_MEMORY_MMAP ) // reqBuffers requests that the device allocates a `count` diff --git a/examples/device_info/devinfo.go b/examples/device_info/devinfo.go index c2134ca..0d0cd5f 100644 --- a/examples/device_info/devinfo.go +++ b/examples/device_info/devinfo.go @@ -266,7 +266,6 @@ func printCaptureParam(dev *device2.Device) error { return nil } - func printOutputParam(dev *device2.Device) error { params, err := dev.GetStreamParam() if err != nil { diff --git a/examples/format/devfmt.go b/examples/format/devfmt.go index 753e015..f383a5b 100644 --- a/examples/format/devfmt.go +++ b/examples/format/devfmt.go @@ -55,7 +55,7 @@ func main() { log.Printf("current frame rate: %d fps", fps) // update fps if fps < 30 { - if err := device.SetFrameRate(30); err != nil{ + if err := device.SetFrameRate(30); err != nil { log.Fatalf("failed to set frame rate: %s", err) } } @@ -64,4 +64,4 @@ func main() { log.Fatalf("failed to get fps: %s", err) } log.Printf("updated frame rate: %d fps", fps) -} \ No newline at end of file +} diff --git a/v4l2/capability.go b/v4l2/capability.go index fff796c..55fd573 100644 --- a/v4l2/capability.go +++ b/v4l2/capability.go @@ -107,7 +107,7 @@ type Capability struct { Card string // BusInfo is the name of the device bus - BusInfo string + BusInfo string // Version is the kernel version Version uint32 @@ -116,7 +116,7 @@ type Capability struct { Capabilities uint32 // DeviceCapabilities is the capability for this particular (opened) device or node - DeviceCapabilities uint32 + DeviceCapabilities uint32 } // GetCapability retrieves capability info for device @@ -145,42 +145,42 @@ func (c Capability) GetCapabilities() uint32 { // IsVideoCaptureSupported returns caps & CapVideoCapture func (c Capability) IsVideoCaptureSupported() bool { - return c.Capabilities & CapVideoCapture != 0 + return c.Capabilities&CapVideoCapture != 0 } // IsVideoOutputSupported returns caps & CapVideoOutput func (c Capability) IsVideoOutputSupported() bool { - return c.Capabilities & CapVideoOutput != 0 + return c.Capabilities&CapVideoOutput != 0 } // IsVideoOverlaySupported returns caps & CapVideoOverlay func (c Capability) IsVideoOverlaySupported() bool { - return c.Capabilities & CapVideoOverlay != 0 + return c.Capabilities&CapVideoOverlay != 0 } // IsVideoOutputOverlaySupported returns caps & CapVideoOutputOverlay func (c Capability) IsVideoOutputOverlaySupported() bool { - return c.Capabilities & CapVideoOutputOverlay != 0 + return c.Capabilities&CapVideoOutputOverlay != 0 } // IsVideoCaptureMultiplanarSupported returns caps & CapVideoCaptureMPlane func (c Capability) IsVideoCaptureMultiplanarSupported() bool { - return c.Capabilities & CapVideoCaptureMPlane != 0 + return c.Capabilities&CapVideoCaptureMPlane != 0 } // IsVideoOutputMultiplanerSupported returns caps & CapVideoOutputMPlane func (c Capability) IsVideoOutputMultiplanerSupported() bool { - return c.Capabilities & CapVideoOutputMPlane != 0 + return c.Capabilities&CapVideoOutputMPlane != 0 } // IsReadWriteSupported returns caps & CapReadWrite func (c Capability) IsReadWriteSupported() bool { - return c.Capabilities & CapReadWrite != 0 + return c.Capabilities&CapReadWrite != 0 } // IsStreamingSupported returns caps & CapStreaming func (c Capability) IsStreamingSupported() bool { - return c.Capabilities & CapStreaming != 0 + return c.Capabilities&CapStreaming != 0 } // IsDeviceCapabilitiesProvided returns true if the device returns @@ -188,7 +188,7 @@ func (c Capability) IsStreamingSupported() bool { // See notes on VL42_CAP_DEVICE_CAPS: // https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/vidioc-querycap.html?highlight=v4l2_cap_device_caps func (c Capability) IsDeviceCapabilitiesProvided() bool { - return c.Capabilities & CapDeviceCapabilities != 0 + return c.Capabilities&CapDeviceCapabilities != 0 } // GetDriverCapDescriptions return textual descriptions of driver capabilities diff --git a/v4l2/controls.go b/v4l2/controls.go index bfe3739..b91581a 100644 --- a/v4l2/controls.go +++ b/v4l2/controls.go @@ -64,7 +64,7 @@ func GetExtControls(fd uintptr, controls []ExtControl) (ExtControls, error) { // prepare control requests var Cctrls []C.struct_v4l2_ext_control - for _, control := range controls{ + for _, control := range controls { var Cctrl C.struct_v4l2_ext_control Cctrl.id = C.uint(control.ID) Cctrl.size = C.uint(control.Size) @@ -86,7 +86,7 @@ func GetExtControls(fd uintptr, controls []ExtControl) (ExtControls, error) { Cctrls = *(*[]C.struct_v4l2_ext_control)(unsafe.Pointer(&ctrls.controls)) for _, Cctrl := range Cctrls { extCtrl := ExtControl{ - ID: uint32(Cctrl.id), + ID: uint32(Cctrl.id), Size: uint32(Cctrl.size), Ctrl: *(*ExtControlUnion)(unsafe.Pointer(&Cctrl.anon0[0])), } @@ -94,4 +94,4 @@ func GetExtControls(fd uintptr, controls []ExtControl) (ExtControls, error) { } return retCtrls, nil -} \ No newline at end of file +} diff --git a/v4l2/controls_mpeg2.go b/v4l2/controls_mpeg2.go index 1026eea..931a179 100644 --- a/v4l2/controls_mpeg2.go +++ b/v4l2/controls_mpeg2.go @@ -27,8 +27,8 @@ type ControlMPEG2Picture struct { // ControlMPEG2Quantization (v4l2_ctrl_mpeg2_quantisation) // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/v4l2-controls.h#L1972 type ControlMPEG2Quantization struct { - IntraQuantizerMatrix [64]uint8 - NonIntraQuantizerMatrix [64]uint8 - ChromaIntraQuantizerMatrix [64]uint8 + IntraQuantizerMatrix [64]uint8 + NonIntraQuantizerMatrix [64]uint8 + ChromaIntraQuantizerMatrix [64]uint8 ChromaNonIntraQuantizerMatrix [64]uint8 -} \ No newline at end of file +} diff --git a/v4l2/dimension.go b/v4l2/dimension.go index aed02e6..ebd47d2 100644 --- a/v4l2/dimension.go +++ b/v4l2/dimension.go @@ -3,7 +3,7 @@ package v4l2 // Area (v4l2_area) // See https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L424 type Area struct { - Width uint32 + Width uint32 Height uint32 } @@ -24,4 +24,3 @@ type Rect struct { Width uint32 Height uint32 } - diff --git a/v4l2/format_frameintervals.go b/v4l2/format_frameintervals.go index b9612a4..573e53f 100644 --- a/v4l2/format_frameintervals.go +++ b/v4l2/format_frameintervals.go @@ -46,7 +46,7 @@ type FrameInterval struct { // } // See https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-frameintervals.html -func getFrameInterval(interval C.struct_v4l2_frmivalenum) (FrameIntervalEnum,error) { +func getFrameInterval(interval C.struct_v4l2_frmivalenum) (FrameIntervalEnum, error) { frmInterval := FrameIntervalEnum{ Index: uint32(interval.index), Type: FrameIntervalType(interval._type), diff --git a/v4l2/format_framesizes.go b/v4l2/format_framesizes.go index 3f5934f..ddcd7f0 100644 --- a/v4l2/format_framesizes.go +++ b/v4l2/format_framesizes.go @@ -22,10 +22,10 @@ const ( // https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/videodev2.h#L829 // https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-enum-framesizes.html type FrameSizeEnum struct { - Index uint32 - Type FrameSizeType + Index uint32 + Type FrameSizeType PixelFormat FourCCType - Size FrameSize + Size FrameSize } // FrameSizeDiscrete (v4l2_frmsize_discrete) @@ -66,7 +66,7 @@ func getFrameSize(frmSizeEnum C.struct_v4l2_frmsizeenum) FrameSizeEnum { frameSize.Size.MaxHeight = fsDiscrete.Height case FrameSizeTypeStepwise, FrameSizeTypeContinuous: // Calculate pointer to access stepwise member - frameSize.Size = *(*FrameSize)(unsafe.Pointer(uintptr(unsafe.Pointer(&frmSizeEnum.anon0[0]))+unsafe.Sizeof(FrameSizeDiscrete{}))) + frameSize.Size = *(*FrameSize)(unsafe.Pointer(uintptr(unsafe.Pointer(&frmSizeEnum.anon0[0])) + unsafe.Sizeof(FrameSizeDiscrete{}))) default: } return frameSize diff --git a/v4l2/stream_param.go b/v4l2/stream_param.go index 040cf79..0491722 100644 --- a/v4l2/stream_param.go +++ b/v4l2/stream_param.go @@ -24,7 +24,7 @@ const ( type StreamParam struct { Type IOType Capture CaptureParam - Output OutputParam + Output OutputParam } // CaptureParam (v4l2_captureparm) @@ -47,7 +47,7 @@ type OutputParam struct { CaptureMode StreamParamFlag TimePerFrame Fract ExtendedMode uint32 - WriteBuffers uint32 + WriteBuffers uint32 _ [4]uint32 } @@ -88,4 +88,4 @@ func SetStreamParam(fd uintptr, bufType BufType, param StreamParam) error { } return nil -} \ No newline at end of file +} diff --git a/v4l2/streaming.go b/v4l2/streaming.go index fc1c06d..573a404 100644 --- a/v4l2/streaming.go +++ b/v4l2/streaming.go @@ -186,7 +186,7 @@ func mapMemoryBuffer(fd uintptr, offset int64, len int) ([]byte, error) { } // MapMemoryBuffers creates mapped memory buffers for specified buffer count of device. -func MapMemoryBuffers(dev StreamingDevice)([][]byte, error) { +func MapMemoryBuffers(dev StreamingDevice) ([][]byte, error) { bufCount := int(dev.BufferCount()) buffers := make([][]byte, bufCount) for i := 0; i < bufCount; i++ { diff --git a/v4l2/types.go b/v4l2/types.go index 7c3dfb0..4beb440 100644 --- a/v4l2/types.go +++ b/v4l2/types.go @@ -36,4 +36,4 @@ type StreamingDevice interface { //type OutputDevice interface { // StreamingDevice // StartOutput(context.Context, chan<- []byte) error -//} \ No newline at end of file +//} diff --git a/v4l2/version.go b/v4l2/version.go index 10fcd3e..5fa7a79 100644 --- a/v4l2/version.go +++ b/v4l2/version.go @@ -8,16 +8,16 @@ type VersionInfo struct { value uint32 } -func (v VersionInfo) Major() uint32{ +func (v VersionInfo) Major() uint32 { return v.value >> 16 } -func (v VersionInfo) Minor() uint32{ - return (v.value>>8)&0xff +func (v VersionInfo) Minor() uint32 { + return (v.value >> 8) & 0xff } -func (v VersionInfo) Patch() uint32{ - return v.value&0xff +func (v VersionInfo) Patch() uint32 { + return v.value & 0xff } // Value returns the raw numeric version value @@ -27,4 +27,4 @@ func (v VersionInfo) Value() uint32 { func (v VersionInfo) String() string { return fmt.Sprintf("v%d.%d.%d", v.Major(), v.Minor(), v.Patch()) -} \ No newline at end of file +} diff --git a/v4l2/video_info.go b/v4l2/video_info.go index fa29b93..f9a7089 100644 --- a/v4l2/video_info.go +++ b/v4l2/video_info.go @@ -17,7 +17,7 @@ type InputStatus = uint32 var ( InputStatusNoPower InputStatus = C.V4L2_IN_ST_NO_POWER InputStatusNoSignal InputStatus = C.V4L2_IN_ST_NO_SIGNAL - InputStatusNoColor InputStatus = C.V4L2_IN_ST_NO_COLOR + InputStatusNoColor InputStatus = C.V4L2_IN_ST_NO_COLOR ) var InputStatuses = map[InputStatus]string{ From f49535ec1c07a989d24e80854a1686df2aabdf55 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sat, 21 May 2022 11:32:49 -0400 Subject: [PATCH 10/11] Update readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4b40e0a..b0dff7b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/vladimirvivien/go4vl)](https://goreportcard.com/report/github.com/vladimirvivien/go4vl) # go4vl + A Go library for the `Video for Linux 2` (v4l2) user API. ---- @@ -12,6 +13,7 @@ It hides all the complexities of working with V4L2 and provides idiomatic Go typ > It is *NOT* meant to be a portable/cross-platform capable package for real-time video processing. ## Features + * Capture and control video data from your Go programs * Idiomatic Go types such as channels to access and stream video data * Exposes device enumeration and information @@ -20,6 +22,7 @@ It hides all the complexities of working with V4L2 and provides idiomatic Go typ * Streaming users zero-copy IO using memory mapped buffers ## Compilation Requirements + * Go compiler/tools * Kernel minimum v5.10.x * A locally configured C compiler (i.e. gcc) From 2ccc9fcfedee7f179e4ce4e52d3aabbfc10dec33 Mon Sep 17 00:00:00 2001 From: Vladimir Vivien Date: Sat, 21 May 2022 11:42:02 -0400 Subject: [PATCH 11/11] Update main README with proper example --- README.md | 61 +++++++++++++++++++++---------------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b0dff7b..e18695d 100644 --- a/README.md +++ b/README.md @@ -59,47 +59,33 @@ and saves the captured frames as JPEG files. The example assumes the attached device supports JPEG (MJPEG) output format inherently. ```go -package main - -import ( - ... - "github.com/vladimirvivien/go4vl/v4l2" -) - func main() { + devName := "/dev/video0" + flag.StringVar(&devName, "d", devName, "device name (path)") + flag.Parse() + // open device - device, err := v4l2.Open("/dev/video0") + device, err := device.Open( + devName, + device.WithPixFormat(v4l2.PixFormat{PixelFormat: v4l2.PixelFmtMPEG, Width: 640, Height: 480}), + ) if err != nil { log.Fatalf("failed to open device: %s", err) } defer device.Close() - // configure device with preferred fmt - if err := device.SetPixFormat(v4l2.PixFormat{ - Width: 640, - Height: 480, - PixelFormat: v4l2.PixelFmtMJPEG, - Field: v4l2.FieldNone, - }); err != nil { - log.Fatalf("failed to set format: %s", err) - } - - // start a device stream with 3 video buffers - if err := device.StartStream(3); err != nil { + // start stream with cancellable context + ctx, stop := context.WithCancel(context.TODO()) + if err := device.Start(ctx); err != nil { log.Fatalf("failed to start stream: %s", err) } - ctx, cancel := context.WithCancel(context.TODO()) - // capture video data at 15 fps - frameChan, err := device.Capture(ctx, 15) - if err != nil { - log.Fatal(err) - } - - // grab 10 frames from frame channel and save them as files + // process frames from capture channel totalFrames := 10 count := 0 - for frame := range frameChan { + log.Printf("Capturing %d frames...", totalFrames) + + for frame := range device.GetOutput() { fileName := fmt.Sprintf("capture_%d.jpg", count) file, err := os.Create(fileName) if err != nil { @@ -110,6 +96,7 @@ func main() { log.Printf("failed to write file %s: %s", fileName, err) continue } + log.Printf("Saved file: %s", fileName) if err := file.Close(); err != nil { log.Printf("failed to close file %s: %s", fileName, err) } @@ -119,20 +106,18 @@ func main() { } } - cancel() // stop capture - if err := device.StopStream(); err != nil { - log.Fatal(err) - } + stop() // stop capture fmt.Println("Done.") } ``` -### Other examples -The [./examples](./examples) directory contains additional examples including: +> Read a detail walk-through about this example [here](./examples/capture0/README.md). -* [device_info](./examples/device_info) - queries and prints devince information -* [webcam](./examples/webcam) - uses the v4l2 package to create a simple webcam that streams images from an attached camera accessible via a web page. +### Other examples +The [./examples](./examples/README.md) directory contains additional examples including: +* [device_info](./examples/device_info/README.md) - queries and prints video device information +* [webcam](./examples/webcam/README.md) - uses the v4l2 package to create a simple webcam that streams images from an attached camera accessible via a web page. ## Roadmap -There is no defined roadmap. The main goal is to port as much functionlities as possible so that +The main goal is to port as many functionalities as possible so that adopters can use Go to create cool video-based tools on platforms such as the Raspberry Pi. \ No newline at end of file