Skip to content
Merged
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 @@ -55,7 +55,7 @@ std::vector<jsi::PropNameID> FrameHostObject::getPropertyNames(jsi::Runtime& rt)
return result;
}

#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value
#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count)->jsi::Value

jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
auto name = propName.utf8(runtime);
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"
}
}
2 changes: 1 addition & 1 deletion package/ios/Core/CameraConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ final class CameraConfiguration {
case disabled
case enabled(config: T)

public static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool {
static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool {
switch (lhs, rhs) {
case (.disabled, .disabled):
return true
Expand Down
22 changes: 21 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,23 @@ 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
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
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -265,7 +266,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat
}
}

public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
switch captureOutput {
case is AVCaptureVideoDataOutput:
onVideoFrame(sampleBuffer: sampleBuffer, orientation: connection.orientation, isMirrored: connection.isVideoMirrored)
Expand Down
2 changes: 1 addition & 1 deletion package/ios/Core/PreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class PreviewView: UIView {
}
}

override public static var layerClass: AnyClass {
override static var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}

Expand Down
2 changes: 1 addition & 1 deletion package/ios/Core/Recording/Track.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ final class Track {
/**
Returns the last timestamp that was actually written to the track.
*/
public private(set) var lastTimestamp: CMTime?
private(set) var lastTimestamp: CMTime?

/**
Gets the natural size of the asset writer, or zero if it is not a visual track.
Expand Down
8 changes: 4 additions & 4 deletions package/ios/Core/Recording/TrackTimeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ final class TrackTimeline {
Represents whether the timeline has been marked as finished or not.
A timeline will automatically be marked as finished when a timestamp arrives that appears after a stop().
*/
public private(set) var isFinished = false
private(set) var isFinished = false

/**
Gets the latency of the buffers in this timeline.
This is computed by (currentTime - mostRecentBuffer.timestamp)
*/
public private(set) var latency: CMTime = .zero
private(set) var latency: CMTime = .zero

/**
Get the first actually written timestamp of this timeline
*/
public private(set) var firstTimestamp: CMTime?
private(set) var firstTimestamp: CMTime?
/**
Get the last actually written timestamp of this timeline.
*/
public private(set) var lastTimestamp: CMTime?
private(set) var lastTimestamp: CMTime?

init(ofTrackType type: TrackType, withClock clock: CMClock) {
trackType = type
Expand Down
2 changes: 1 addition & 1 deletion package/ios/FrameProcessors/FrameHostObject.mm
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
return result;
}

#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value
#define JSI_FUNC [=](jsi::Runtime & runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count)->jsi::Value

jsi::Value FrameHostObject::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) {
auto name = propName.utf8(runtime);
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
2 changes: 1 addition & 1 deletion package/ios/React/Utils/Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Foundation
* Represents a JavaScript Promise instance. `reject()` and `resolve()` should only be called once.
*/
class Promise {
public private(set) var didResolve = false
private(set) var didResolve = false

init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
self.resolver = resolver
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
13 changes: 13 additions & 0 deletions package/src/types/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,5 +416,18 @@ export interface CameraProps extends ViewProps {
* ```
*/
codeScanner?: CodeScanner
/**
* Fires every few hundred milliseconds to notify how many
* total bytes are currently written to the video file.
* @example
* ```tsx
* const onBytesWrittenVideo = (bytes: number) => {
* console.log(`Bytes written: ${bytes}`)
* }
*
* return <Camera {...props} onBytesWrittenVideo={onBytesWrittenVideo} />
* ```
*/
onBytesWrittenVideo?: (bytes: number) => void
//#endregion
}
7 changes: 7 additions & 0 deletions package/src/types/VideoFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ export interface VideoFile extends TemporaryFile {
*/
height: number
}

export interface OnBytesWrittenVideoEvent {
/**
* Represents the amount of total bytes written to the video file.
*/
bytesWritten: number
}