Skip to content

Commit f4d878e

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
refactor: extract camera capture logic into reusable library packages
Move gralloc allocation, IGBP stub, and camera connection/capture helpers out of examples/camera_capture/main.go into library packages: - camera/gralloc: gralloc buffer allocation via IAllocator HAL - camera/igbp: IGraphicBufferProducer stub with gralloc buffers - camera/: high-level Device API (Connect, ConfigureStream, CaptureFrame, Close) The example shrinks from 917 to 81 lines. Both cmd/bindercli and cmd/camera_fwk now import the shared libraries instead of duplicating the raw transaction helpers and wire-format functions.
1 parent 00220bc commit f4d878e

13 files changed

Lines changed: 1048 additions & 1800 deletions

File tree

camera/device.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package camera
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
gfxCommon "github.com/xaionaro-go/binder/android/hardware/graphics/common"
10+
11+
fwkDevice "github.com/xaionaro-go/binder/android/frameworks/cameraservice/device"
12+
fwkService "github.com/xaionaro-go/binder/android/frameworks/cameraservice/service"
13+
"github.com/xaionaro-go/binder/binder"
14+
cameraIGBP "github.com/xaionaro-go/binder/camera/igbp"
15+
"github.com/xaionaro-go/binder/camera/gralloc"
16+
"github.com/xaionaro-go/binder/servicemanager"
17+
)
18+
19+
// Device represents a connected camera device with a configured capture
20+
// stream. Use Connect to create, ConfigureStream to set up the stream,
21+
// CaptureFrame to read frames, and Close to disconnect.
22+
type Device struct {
23+
sm *servicemanager.ServiceManager
24+
transport binder.Transport
25+
26+
deviceUser fwkDevice.ICameraDeviceUser
27+
callback *deviceCallback
28+
29+
// Set after ConfigureStream.
30+
igbpStub *cameraIGBP.ProducerStub
31+
grallocBufs [4]*gralloc.Buffer
32+
streamID int32
33+
metadata []byte
34+
width int32
35+
height int32
36+
}
37+
38+
// Connect opens a connection to the camera device identified by cameraID
39+
// (typically "0" for the back camera).
40+
func Connect(
41+
ctx context.Context,
42+
sm *servicemanager.ServiceManager,
43+
transport binder.Transport,
44+
cameraID string,
45+
) (_ *Device, _err error) {
46+
svc, err := sm.GetService(ctx, "android.frameworks.cameraservice.service.ICameraService/default")
47+
if err != nil {
48+
return nil, fmt.Errorf("getting camera service: %w", err)
49+
}
50+
51+
proxy := fwkService.NewCameraServiceProxy(svc)
52+
cb := &deviceCallback{}
53+
stub := fwkDevice.NewCameraDeviceCallbackStub(cb)
54+
55+
stubBinder := stub.AsBinder().(*binder.StubBinder)
56+
stubBinder.RegisterWithTransport(ctx, transport)
57+
time.Sleep(100 * time.Millisecond)
58+
59+
deviceUser, err := proxy.ConnectDevice(ctx, stub, cameraID)
60+
if err != nil {
61+
return nil, fmt.Errorf("ConnectDevice: %w", err)
62+
}
63+
64+
dev := &Device{
65+
sm: sm,
66+
transport: transport,
67+
deviceUser: deviceUser,
68+
callback: cb,
69+
}
70+
71+
return dev, nil
72+
}
73+
74+
// ConfigureStream sets up a capture stream with the given dimensions and
75+
// pixel format. It allocates gralloc buffers, creates an IGBP surface
76+
// stub, and configures the camera for streaming.
77+
func (d *Device) ConfigureStream(
78+
ctx context.Context,
79+
width int32,
80+
height int32,
81+
format Format,
82+
) error {
83+
d.width = width
84+
d.height = height
85+
86+
// Allocate gralloc buffers.
87+
for i := range d.grallocBufs {
88+
buf, err := gralloc.Allocate(
89+
ctx,
90+
d.sm,
91+
width,
92+
height,
93+
format,
94+
gfxCommon.BufferUsageCpuReadOften|gfxCommon.BufferUsageCameraOutput,
95+
)
96+
if err != nil {
97+
return fmt.Errorf("allocating gralloc buffer %d: %w", i, err)
98+
}
99+
if err := buf.Mmap(); err != nil {
100+
return fmt.Errorf("mmap gralloc buffer %d: %w", i, err)
101+
}
102+
d.grallocBufs[i] = buf
103+
}
104+
105+
// Begin configuration.
106+
if err := d.deviceUser.BeginConfigure(ctx); err != nil {
107+
return fmt.Errorf("BeginConfigure: %w", err)
108+
}
109+
110+
// Get default request metadata.
111+
metadata, err := CreateDefaultRequest(ctx, d.deviceUser, fwkDevice.TemplateIdPREVIEW)
112+
if err != nil {
113+
return fmt.Errorf("CreateDefaultRequest: %w", err)
114+
}
115+
d.metadata = metadata
116+
117+
// Create IGBP stub and stream.
118+
d.igbpStub = cameraIGBP.NewProducerStub(uint32(width), uint32(height), d.grallocBufs)
119+
igbpStubBinder := binder.NewStubBinder(d.igbpStub)
120+
igbpStubBinder.RegisterWithTransport(ctx, d.transport)
121+
122+
streamID, err := CreateStreamWithSurface(ctx, d.deviceUser, d.transport, igbpStubBinder, width, height)
123+
if err != nil {
124+
return fmt.Errorf("CreateStream: %w", err)
125+
}
126+
d.streamID = streamID
127+
128+
// End configuration.
129+
if err := d.deviceUser.EndConfigure(
130+
ctx,
131+
fwkDevice.StreamConfigurationModeNormalMode,
132+
fwkDevice.CameraMetadata{Metadata: []byte{}},
133+
0,
134+
); err != nil {
135+
return fmt.Errorf("EndConfigure: %w", err)
136+
}
137+
138+
return nil
139+
}
140+
141+
// CaptureFrame captures a single frame and returns the raw pixel data.
142+
// The caller should have called ConfigureStream first. This method
143+
// submits a repeating capture request on the first call, then reads
144+
// from the IGBP queue channel on subsequent calls.
145+
func (d *Device) CaptureFrame(
146+
ctx context.Context,
147+
) ([]byte, error) {
148+
if d.igbpStub == nil {
149+
return nil, fmt.Errorf("stream not configured; call ConfigureStream first")
150+
}
151+
152+
// Submit a repeating capture request (only needs to happen once,
153+
// but submitting again is harmless and simplifies the API).
154+
captureReq := fwkDevice.CaptureRequest{
155+
PhysicalCameraSettings: []fwkDevice.PhysicalCameraSettings{
156+
{
157+
Id: "0",
158+
Settings: fwkDevice.CaptureMetadataInfo{
159+
Tag: fwkDevice.CaptureMetadataInfoTagMetadata,
160+
Metadata: fwkDevice.CameraMetadata{Metadata: d.metadata},
161+
},
162+
},
163+
},
164+
StreamAndWindowIds: []fwkDevice.StreamAndWindowId{
165+
{StreamId: d.streamID, WindowId: 0},
166+
},
167+
}
168+
169+
_, err := SubmitRequest(ctx, d.deviceUser, captureReq, true)
170+
if err != nil {
171+
// Fallback to single shot.
172+
_, err = SubmitRequest(ctx, d.deviceUser, captureReq, false)
173+
if err != nil {
174+
return nil, fmt.Errorf("SubmitRequestList: %w", err)
175+
}
176+
}
177+
178+
// Wait for a frame to be queued.
179+
select {
180+
case <-ctx.Done():
181+
return nil, ctx.Err()
182+
case slot := <-d.igbpStub.QueuedFrames():
183+
buf := d.igbpStub.SlotBuffer(slot)
184+
if buf == nil || buf.MmapData == nil {
185+
return nil, fmt.Errorf("slot %d has no mmap'd buffer", slot)
186+
}
187+
// Return a copy so the caller owns the data.
188+
frame := make([]byte, len(buf.MmapData))
189+
copy(frame, buf.MmapData)
190+
return frame, nil
191+
}
192+
}
193+
194+
// Close disconnects from the camera device and releases gralloc buffers.
195+
func (d *Device) Close(ctx context.Context) error {
196+
var errs []error
197+
198+
if d.deviceUser != nil {
199+
if err := d.deviceUser.Disconnect(ctx); err != nil {
200+
errs = append(errs, fmt.Errorf("disconnect: %w", err))
201+
}
202+
}
203+
204+
for _, buf := range d.grallocBufs {
205+
if buf != nil {
206+
buf.Munmap()
207+
}
208+
}
209+
210+
return errors.Join(errs...)
211+
}

camera/device_callback.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Package camera provides a high-level API for capturing frames from an
2+
// Android camera via binder.
3+
package camera
4+
5+
import (
6+
"context"
7+
"sync"
8+
9+
fwkDevice "github.com/xaionaro-go/binder/android/frameworks/cameraservice/device"
10+
)
11+
12+
// deviceCallback implements fwkDevice.ICameraDeviceCallback, tracking
13+
// how many frames the camera service has started processing.
14+
type deviceCallback struct {
15+
mu sync.Mutex
16+
framesReceived int
17+
}
18+
19+
func (c *deviceCallback) OnCaptureStarted(
20+
_ context.Context,
21+
_ fwkDevice.CaptureResultExtras,
22+
_ int64,
23+
) error {
24+
c.mu.Lock()
25+
defer c.mu.Unlock()
26+
c.framesReceived++
27+
return nil
28+
}
29+
30+
func (c *deviceCallback) OnDeviceError(
31+
_ context.Context,
32+
_ fwkDevice.ErrorCode,
33+
_ fwkDevice.CaptureResultExtras,
34+
) error {
35+
return nil
36+
}
37+
38+
func (c *deviceCallback) OnDeviceIdle(_ context.Context) error {
39+
return nil
40+
}
41+
42+
func (c *deviceCallback) OnPrepared(_ context.Context, _ int32) error {
43+
return nil
44+
}
45+
46+
func (c *deviceCallback) OnRepeatingRequestError(
47+
_ context.Context,
48+
_ int64,
49+
_ int32,
50+
) error {
51+
return nil
52+
}
53+
54+
func (c *deviceCallback) OnResultReceived(
55+
_ context.Context,
56+
_ fwkDevice.CaptureMetadataInfo,
57+
_ fwkDevice.CaptureResultExtras,
58+
_ []fwkDevice.PhysicalCaptureResultInfo,
59+
) error {
60+
return nil
61+
}

camera/format.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package camera
2+
3+
import gfxCommon "github.com/xaionaro-go/binder/android/hardware/graphics/common"
4+
5+
// Format identifies a pixel format for camera capture.
6+
type Format = gfxCommon.PixelFormat
7+
8+
// Supported pixel formats for camera capture.
9+
const (
10+
FormatYCbCr420 Format = gfxCommon.PixelFormatYcbcr420888
11+
)

camera/gralloc/allocate.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package gralloc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/xaionaro-go/binder/android/hardware/graphics/allocator"
8+
gfxCommon "github.com/xaionaro-go/binder/android/hardware/graphics/common"
9+
"github.com/xaionaro-go/binder/servicemanager"
10+
)
11+
12+
// Allocate allocates a gralloc buffer using the IAllocator HAL service.
13+
// The returned Buffer contains a dmabuf FD that can be mmap'd for CPU
14+
// read access.
15+
func Allocate(
16+
ctx context.Context,
17+
sm *servicemanager.ServiceManager,
18+
width int32,
19+
height int32,
20+
format gfxCommon.PixelFormat,
21+
usage gfxCommon.BufferUsage,
22+
) (*Buffer, error) {
23+
svc, err := sm.GetService(ctx, "android.hardware.graphics.allocator.IAllocator/default")
24+
if err != nil {
25+
return nil, fmt.Errorf("get allocator service: %w", err)
26+
}
27+
28+
proxy := allocator.NewAllocatorProxy(svc)
29+
30+
desc := allocator.BufferDescriptorInfo{
31+
Name: []byte("camera-buffer"),
32+
Width: width,
33+
Height: height,
34+
LayerCount: 1,
35+
Format: format,
36+
Usage: usage,
37+
ReservedSize: 0,
38+
AdditionalOptions: []gfxCommon.ExtendableType{},
39+
}
40+
41+
result, err := proxy.Allocate2(ctx, desc, 1)
42+
if err != nil {
43+
return nil, fmt.Errorf("Allocate2: %w", err)
44+
}
45+
46+
if len(result.Buffers) == 0 {
47+
return nil, fmt.Errorf("Allocate2 returned 0 buffers")
48+
}
49+
50+
return &Buffer{
51+
Handle: result.Buffers[0],
52+
Stride: result.Stride,
53+
Width: uint32(width),
54+
Height: uint32(height),
55+
Format: int32(format),
56+
Usage: uint64(usage),
57+
}, nil
58+
}

camera/gralloc/buffer.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Package gralloc provides gralloc buffer allocation via the Android
2+
// IAllocator HAL service.
3+
package gralloc
4+
5+
import (
6+
"fmt"
7+
8+
common "github.com/xaionaro-go/binder/android/hardware/common"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
// Buffer holds a gralloc-allocated buffer with its NativeHandle.
14+
type Buffer struct {
15+
Handle common.NativeHandle
16+
Stride int32
17+
Width uint32
18+
Height uint32
19+
Format int32
20+
Usage uint64
21+
22+
// MmapData holds a persistent mmap of the dmabuf, set by Mmap().
23+
// Keeping it mapped avoids mmap/munmap syscalls per frame read.
24+
MmapData []byte
25+
}
26+
27+
// Mmap creates a persistent read-only mmap of this buffer's dmabuf FD.
28+
// The MmapData field can then be read directly. Call Munmap to release.
29+
func (b *Buffer) Mmap() error {
30+
if len(b.Handle.Fds) == 0 {
31+
return fmt.Errorf("no FDs in gralloc buffer")
32+
}
33+
fd := int(b.Handle.Fds[0])
34+
// YCbCr_420_888: Y plane (w*h) + CbCr interleaved (w*h/2).
35+
bufSize := int(b.Width) * int(b.Height) * 3 / 2
36+
data, err := unix.Mmap(fd, 0, bufSize, unix.PROT_READ, unix.MAP_SHARED)
37+
if err != nil {
38+
return fmt.Errorf("mmap fd=%d size=%d: %w", fd, bufSize, err)
39+
}
40+
b.MmapData = data
41+
return nil
42+
}
43+
44+
// Munmap releases the persistent mmap created by Mmap.
45+
func (b *Buffer) Munmap() {
46+
if b.MmapData != nil {
47+
_ = unix.Munmap(b.MmapData)
48+
b.MmapData = nil
49+
}
50+
}

0 commit comments

Comments
 (0)