Skip to content

Feature | Support onBytesWrittenVideo to receive bytes on JS while recording Video #3549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/guides/RECORDING_VIDEOS.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ To start a video recording you first have to enable video capture:
{...props}
video={true}
audio={true} // <-- optional
onBytesWrittenVideo={(bytes) => {/*Whatever you need with bytes in realtime while it is recording*/}} // <-- optional
/>
```

Expand Down
1 change: 1 addition & 0 deletions example/src/CameraPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
ref={camera}
onInitialized={onInitialized}
onError={onError}
onBytesWrittenVideo={(bytes) => console.log(`Bytes written: ${bytes / 1024 / 1024} MB!`)}
onStarted={() => console.log('Camera started!')}
onStopped={() => console.log('Camera stopped!')}
onPreviewStarted={() => console.log('Preview started!')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ fun CameraSession.startRecording(
enableAudio: Boolean,
options: RecordVideoOptions,
callback: (video: Video) -> Unit,
onError: (error: CameraError) -> Unit
onError: (error: CameraError) -> Unit,
onBytesWrittenCallback: (bytes: Long) -> Unit
) {
if (camera == null) throw CameraNotReadyError()
if (recording != null) throw RecordingInProgressError()
Expand Down Expand Up @@ -49,7 +50,10 @@ fun CameraSession.startRecording(

is VideoRecordEvent.Pause -> Log.i(CameraSession.TAG, "Recording paused!")

is VideoRecordEvent.Status -> Log.i(CameraSession.TAG, "Status update! Recorded ${event.recordingStats.numBytesRecorded} bytes.")
is VideoRecordEvent.Status -> {
Log.i(CameraSession.TAG, "Status update! Recorded ${event.recordingStats.numBytesRecorded} bytes.")
onBytesWrittenCallback(event.recordingStats.numBytesRecorded)
}

is VideoRecordEvent.Finalize -> {
if (isRecordingCanceled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,6 @@ class CameraSession(internal val context: Context, internal val callback: Callba
fun onOutputOrientationChanged(outputOrientation: Orientation)
fun onPreviewOrientationChanged(previewOrientation: Orientation)
fun onCodeScanned(codes: List<Barcode>, scannerFrame: CodeScannerFrame)
fun onBytesWrittenVideo(bytesWritten: Double)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ fun CameraView.invokeOnAverageFpsChanged(averageFps: Double) {
this.sendEvent(event)
}

fun CameraView.invokeOnBytesWrittenVideo(bytesWritten: Double) {
Log.i(CameraView.TAG, "invokeOnBytesWrittenVideo($bytesWritten)")

val surfaceId = UIManagerHelper.getSurfaceId(this)
val data = Arguments.createMap()
data.putDouble("bytesWritten", bytesWritten)

val event = BytesWrittenVideoEvent(surfaceId, id, data)
this.sendEvent(event)
}

fun CameraView.invokeOnCodeScanned(barcodes: List<Barcode>, scannerFrame: CodeScannerFrame) {
val codes = Arguments.createArray()
barcodes.forEach { barcode ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import com.mrousavy.camera.core.types.Video
import com.mrousavy.camera.react.utils.makeErrorMap

fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) {
// check audio permission
val onBytesWrittenCallback = { bytes: Long ->
this.onBytesWrittenVideo(bytes.toDouble())
}
if (audio) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
throw MicrophonePermissionError()
Expand All @@ -36,7 +38,7 @@ fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Cal
val errorMap = makeErrorMap(error.code, error.message)
onRecordCallback(null, errorMap)
}
cameraSession.startRecording(audio, options, callback, onError)
cameraSession.startRecording(audio, options, callback, onError, onBytesWrittenCallback)
}

fun CameraView.pauseRecording() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,8 @@ class CameraView(context: Context) :
override fun onAverageFpsChanged(averageFps: Double) {
invokeOnAverageFpsChanged(averageFps)
}

override fun onBytesWrittenVideo(bytesWritten: Double) {
invokeOnBytesWrittenVideo(bytesWritten)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
.put(CameraOutputOrientationChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOutputOrientationChanged"))
.put(CameraPreviewOrientationChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPreviewOrientationChanged"))
.put(AverageFpsChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onAverageFpsChanged"))
.put(BytesWrittenVideoEvent.EVENT_NAME, MapBuilder.of("registrationName", "onBytesWrittenVideo"))
.build()

override fun getName(): String = TAG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,11 @@ class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: Writ
const val EVENT_NAME = "topCameraCodeScanned"
}
}
class BytesWrittenVideoEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) :
Event<BytesWrittenVideoEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME
override fun getEventData() = data
companion object {
const val EVENT_NAME = "bytesWrittenVideoEvent"
}
}
25 changes: 24 additions & 1 deletion package/ios/Core/CameraSession+Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ extension CameraSession {
*/
func startRecording(options: RecordVideoOptions,
onVideoRecorded: @escaping (_ video: Video) -> Void,
onError: @escaping (_ error: CameraError) -> Void) {
onError: @escaping (_ error: CameraError) -> Void,
onBytesWritten: @escaping (_ bytes: Double) -> Void) {
// Run on Camera Queue
CameraQueues.cameraQueue.async {
let start = DispatchTime.now()
Expand Down Expand Up @@ -48,6 +49,8 @@ extension CameraSession {
}

self.recordingSession = nil
self.recordingSizeTimer?.cancel()
self.recordingSizeTimer = nil

if self.didCancelRecording {
VisionLogger.log(level: .info, message: "RecordingSession finished because the recording was canceled.")
Expand Down Expand Up @@ -128,6 +131,26 @@ extension CameraSession {
self.didCancelRecording = false
self.recordingSession = recordingSession

let timer = DispatchSource.makeTimerSource(queue: CameraQueues.cameraQueue)
timer.schedule(deadline: .now(), repeating: 0.4)

timer.setEventHandler {
guard let session = self.recordingSession else {
timer.cancel()
return
}

let path = session.url.path
if let size = try? FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber {
let bytes = size.doubleValue

DispatchQueue.main.async {
onBytesWritten(bytes)
}
}
}
self.recordingSizeTimer = timer
self.recordingSizeTimer?.resume()
let end = DispatchTime.now()
VisionLogger.log(level: .info, message: "RecordingSesssion started in \(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000)ms!")
} catch let error as CameraError {
Expand Down
1 change: 1 addition & 0 deletions package/ios/Core/CameraSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat
// State
var metadataProvider = MetadataProvider()
var recordingSession: RecordingSession?
var recordingSizeTimer: DispatchSourceTimer?
var didCancelRecording = false
var orientationManager = OrientationManager()

Expand Down
3 changes: 2 additions & 1 deletion package/ios/React/CameraView+RecordVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud
},
onError: { error in
callback.reject(error: error)
}
},
onBytesWritten: onBytesWrittenVideo
)
} catch {
// Some error occured while initializing VideoSettings
Expand Down
7 changes: 7 additions & 0 deletions package/ios/React/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat
@objc var onOutputOrientationChangedEvent: RCTDirectEventBlock?
@objc var onViewReadyEvent: RCTDirectEventBlock?
@objc var onAverageFpsChangedEvent: RCTDirectEventBlock?
@objc var onBytesWrittenVideoEvent: RCTDirectEventBlock?
@objc var onCodeScannedEvent: RCTDirectEventBlock?

// zoom
Expand Down Expand Up @@ -392,4 +393,10 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat
"averageFps": averageFps,
])
}

func onBytesWrittenVideo(bytes: Double) {
onBytesWrittenVideoEvent?([
"bytesWritten": bytes,
])
}
}
1 change: 1 addition & 0 deletions package/ios/React/CameraViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage
RCT_REMAP_VIEW_PROPERTY(onPreviewOrientationChanged, onPreviewOrientationChangedEvent, RCTDirectEventBlock);
RCT_REMAP_VIEW_PROPERTY(onViewReady, onViewReadyEvent, RCTDirectEventBlock);
RCT_REMAP_VIEW_PROPERTY(onAverageFpsChanged, onAverageFpsChangedEvent, RCTDirectEventBlock);
RCT_REMAP_VIEW_PROPERTY(onBytesWrittenVideo, onBytesWrittenVideoEvent, RCTDirectEventBlock);
// Code Scanner
RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary);
RCT_REMAP_VIEW_PROPERTY(onCodeScanned, onCodeScannedEvent, RCTDirectEventBlock);
Expand Down
8 changes: 7 additions & 1 deletion package/src/Camera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { CameraProps, DrawableFrameProcessor, OnShutterEvent, ReadonlyFrame
import { CameraModule } from './NativeCameraModule'
import type { PhotoFile, TakePhotoOptions } from './types/PhotoFile'
import type { Point } from './types/Point'
import type { RecordVideoOptions, VideoFile } from './types/VideoFile'
import type { OnBytesWrittenVideoEvent, RecordVideoOptions, VideoFile } from './types/VideoFile'
import { VisionCameraProxy } from './frame-processors/VisionCameraProxy'
import { CameraDevices } from './CameraDevices'
import type { EmitterSubscription, NativeSyntheticEvent, NativeMethods } from 'react-native'
Expand Down Expand Up @@ -102,6 +102,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
this.onPreviewOrientationChanged = this.onPreviewOrientationChanged.bind(this)
this.onError = this.onError.bind(this)
this.onCodeScanned = this.onCodeScanned.bind(this)
this.onBytesWrittenVideo = this.onBytesWrittenVideo.bind(this)
this.ref = React.createRef<RefType>()
this.lastFrameProcessor = undefined
this.state = {
Expand Down Expand Up @@ -599,6 +600,10 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
})
}

private onBytesWrittenVideo({ nativeEvent: { bytesWritten } }: NativeSyntheticEvent<OnBytesWrittenVideoEvent>): void {
this.props.onBytesWrittenVideo?.(bytesWritten)
}

/** @internal */
componentDidUpdate(): void {
if (!this.isNativeViewMounted) return
Expand Down Expand Up @@ -657,6 +662,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
isMirrored={props.isMirrored ?? shouldBeMirrored}
onViewReady={this.onViewReady}
onAverageFpsChanged={enableFpsGraph ? this.onAverageFpsChanged : undefined}
onBytesWrittenVideo={this.onBytesWrittenVideo}
onInitialized={this.onInitialized}
onCodeScanned={this.onCodeScanned}
onStarted={this.onStarted}
Expand Down
3 changes: 3 additions & 0 deletions package/src/NativeCameraView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ErrorWithCause } from './CameraError'
import type { CameraProps, OnShutterEvent } from './types/CameraProps'
import type { Code, CodeScanner, CodeScannerFrame } from './types/CodeScanner'
import type { Orientation } from './types/Orientation'
import type { OnBytesWrittenVideoEvent } from './types/VideoFile'

export interface OnCodeScannedEvent {
codes: Code[]
Expand Down Expand Up @@ -35,6 +36,7 @@ export type NativeCameraViewProps = Omit<
| 'codeScanner'
| 'fps'
| 'videoBitRate'
| 'onBytesWrittenVideo'
> & {
// private intermediate props
cameraId: string
Expand All @@ -58,6 +60,7 @@ export type NativeCameraViewProps = Omit<
onShutter?: (event: NativeSyntheticEvent<OnShutterEvent>) => void
onOutputOrientationChanged?: (event: NativeSyntheticEvent<OutputOrientationChangedEvent>) => void
onPreviewOrientationChanged?: (event: NativeSyntheticEvent<PreviewOrientationChangedEvent>) => void
onBytesWrittenVideo?: (event: NativeSyntheticEvent<OnBytesWrittenVideoEvent>) => void
}

// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
Expand Down
12 changes: 12 additions & 0 deletions package/src/types/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,5 +416,17 @@ export interface CameraProps extends ViewProps {
* ```
*/
codeScanner?: CodeScanner
/**
* Used to get the bytes written to the video file on real time while it is being recorded
* @example
* ```tsx
* const onBytesWrittenVideo = (bytes: number) => {
* console.log(`Bytes written: ${bytes}`)
* }
*
* return <Camera {...props} onBytesWrittenVideo={onBytesWrittenVideo} />
* ```
*/
onBytesWrittenVideo?: (bytes: number) => void
//#endregion
}
4 changes: 4 additions & 0 deletions package/src/types/VideoFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ export interface VideoFile extends TemporaryFile {
*/
height: number
}

export interface OnBytesWrittenVideoEvent {
bytesWritten: number
}
Loading