derbyfish-capture
v1.0.9
Published
Native camera capture module for Derbyfish with IVS livestreaming and on-device analysis
Maintainers
Readme
Derbyfish Capture Module
A production-ready Expo native module for camera capture with AWS IVS livestreaming and on-device analysis capabilities.
Features
- Native Camera Preview: High-performance camera preview using native APIs
- AWS IVS Integration: Livestream to AWS Interactive Video Service (stubbed for implementation)
- On-Device Analysis: Real-time frame analysis with PoLC (Proof of Location Capture) hashing
- 3×3 Grid Scanning: Fish detection scanning with progress tracking
- Motion Integration: Device motion data for enhanced verification
- TypeScript Support: Full TypeScript definitions and type safety
Installation
1. Install the module
npm install derbyfish-capture2. Configure the module
Add the config plugin to your app.config.js:
export default {
// ... your existing config
plugins: [
'derbyfish-capture',
// ... other plugins
],
};3. Prebuild and run
expo prebuild
expo run:ios
# or
expo run:androidUsage
Basic Example
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import {
DerbyfishCaptureView,
startSession,
stopSession,
sendTimedMetadata,
start3x3Scan,
addStatusListener,
addScanListener,
addCommitmentsListener,
type StatusEvent,
type ScanProgressEvent,
type CommitmentsReadyEvent,
} from 'derbyfish-capture';
export default function GoLiveScreen() {
const [isLive, setIsLive] = useState(false);
const [status, setStatus] = useState<StatusEvent>({ live: false });
const [scanProgress, setScanProgress] = useState<ScanProgressEvent | null>(null);
useEffect(() => {
// Listen to status updates
const statusSubscription = addStatusListener((event) => {
setStatus(event);
setIsLive(event.live);
});
// Listen to scan progress
const scanSubscription = addScanListener((event) => {
setScanProgress(event);
});
// Listen to commitments ready
const commitmentsSubscription = addCommitmentsListener((event) => {
Alert.alert(
'Scan Complete',
`Merkle Root: ${event.merkleRoot}\nGrid Root: ${event.gridRoot}\nSPH: ${event.sph}`
);
setScanProgress(null);
});
return () => {
statusSubscription.remove();
scanSubscription.remove();
commitmentsSubscription.remove();
};
}, []);
const handleStartLive = async () => {
try {
await startSession({
bitrateKbps: 3500,
width: 1280,
height: 720,
waterbodyTile: 'lake-michigan-001',
});
} catch (error) {
Alert.alert('Error', 'Failed to start livestream');
}
};
const handleStopLive = async () => {
try {
await stopSession();
} catch (error) {
Alert.alert('Error', 'Failed to stop livestream');
}
};
const handleMarkHook = async () => {
try {
await sendTimedMetadata(JSON.stringify({
type: 'HOOK',
at: Date.now(),
}));
} catch (error) {
Alert.alert('Error', 'Failed to send marker');
}
};
const handleStartScan = async () => {
try {
await start3x3Scan();
} catch (error) {
Alert.alert('Error', 'Failed to start scan');
}
};
return (
<View style={styles.container}>
{/* Camera Preview */}
<DerbyfishCaptureView
style={styles.camera}
showPreview={true}
facing="back"
/>
{/* Status HUD */}
<View style={styles.hud}>
<Text style={styles.hudText}>
LIVE: {status.live ? 'ON' : 'OFF'}
</Text>
<Text style={styles.hudText}>
Bitrate: {status.bitrate ?? '-'} kbps
</Text>
<Text style={styles.hudText}>
FPS: {status.fps ?? '-'}
</Text>
</View>
{/* Scan Progress */}
{scanProgress && (
<View style={styles.scanProgress}>
<Text style={styles.scanText}>
Scan: {scanProgress.index}/{scanProgress.total}
</Text>
</View>
)}
{/* Controls */}
<View style={styles.controls}>
{!isLive ? (
<TouchableOpacity style={[styles.button, styles.startButton]} onPress={handleStartLive}>
<Text style={styles.buttonText}>Go Live</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.button, styles.stopButton]} onPress={handleStopLive}>
<Text style={styles.buttonText}>Stop</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.button} onPress={handleMarkHook}>
<Text style={styles.buttonText}>Mark Hook</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.scanButton]} onPress={handleStartScan}>
<Text style={styles.buttonText}>
Start 3×3 Scan {scanProgress ? `(${scanProgress.index}/${scanProgress.total})` : ''}
</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
camera: {
flex: 1,
},
hud: {
position: 'absolute',
top: 50,
left: 20,
right: 20,
flexDirection: 'row',
justifyContent: 'space-between',
},
hudText: {
color: '#fff',
fontSize: 12,
fontWeight: '600',
},
scanProgress: {
position: 'absolute',
top: 100,
left: 20,
right: 20,
alignItems: 'center',
},
scanText: {
color: '#00ff00',
fontSize: 16,
fontWeight: 'bold',
},
controls: {
position: 'absolute',
bottom: 50,
left: 20,
right: 20,
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
button: {
backgroundColor: '#333',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
minWidth: 80,
alignItems: 'center',
},
startButton: {
backgroundColor: '#007AFF',
},
stopButton: {
backgroundColor: '#FF3B30',
},
scanButton: {
backgroundColor: '#34C759',
},
buttonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
});API Reference
Components
DerbyfishCaptureView
A native camera preview component.
Props:
showPreview?: boolean- Whether to show the camera preview (default: true)facing?: 'front' | 'back'- Camera facing direction (default: 'back')
Functions
startSession(params?: StartSessionParams): Promise<void>
Starts a capture session with the specified parameters.
Parameters:
params.bitrateKbps?: number- Bitrate in kbps for the livestreamparams.width?: number- Video width in pixelsparams.height?: number- Video height in pixelsparams.waterbodyTile?: string- Waterbody tile identifier for geospatial context
stopSession(): Promise<void>
Stops the current capture session.
sendTimedMetadata(json: string): Promise<void>
Sends timed metadata to the livestream.
Parameters:
json: string- JSON string containing the metadata payload
start3x3Scan(): Promise<void>
Starts a 3×3 grid scan for fish detection.
Event Listeners
addStatusListener(listener: (event: StatusEvent) => void): Subscription
Adds a listener for status updates.
Event:
type StatusEvent = {
live: boolean;
bitrate?: number;
fps?: number;
};addScanListener(listener: (event: ScanProgressEvent) => void): Subscription
Adds a listener for scan progress updates.
Event:
type ScanProgressEvent = {
index: number;
total: number;
};addCommitmentsListener(listener: (event: CommitmentsReadyEvent) => void): Subscription
Adds a listener for when scan commitments are ready.
Event:
type CommitmentsReadyEvent = {
merkleRoot: string;
gridRoot: string;
sph: string;
};Implementation Notes
AWS IVS Integration
The module includes stubbed integration points for AWS IVS Broadcast SDK. To complete the integration:
- iOS: Add the IVS Broadcast SDK to your Podfile and implement the TODO sections in
DerbyfishCaptureModule.swift - Android: Add the IVS Broadcast SDK to your build.gradle and implement the TODO sections in
DerbyfishCaptureModule.kt
PoLC Hashing
The module includes a placeholder implementation of Proof of Location Capture (PoLC) hashing. The current implementation:
- Creates deterministic hashes from frame data and motion
- Uses a simple rolling hash (replace with proper Merkle tree implementation)
- Processes frames on background threads to avoid blocking the UI
Permissions
The config plugin automatically adds the following permissions:
iOS (Info.plist):
NSCameraUsageDescriptionNSMicrophoneUsageDescriptionNSLocationWhenInUseUsageDescriptionNSMotionUsageDescription
Android (AndroidManifest.xml):
CAMERARECORD_AUDIOACCESS_FINE_LOCATIONACCESS_COARSE_LOCATIONFOREGROUND_SERVICEFOREGROUND_SERVICE_CAMERAFOREGROUND_SERVICE_MICROPHONE
Development
Building the Module
cd modules/derbyfish-capture
npm run buildRunning Tests
npm testLinting
npm run lintLicense
MIT
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
