Skip to content
Open
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
160 changes: 160 additions & 0 deletions apps/simple-camera/__tests__/visioncamera.video.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type {
CameraDevice,
CameraDeviceFactory,
Recorder,
RecordingFinishedReason,
} from 'react-native-vision-camera'
import { CommonResolutions, VisionCamera } from 'react-native-vision-camera'
Expand Down Expand Up @@ -408,6 +409,165 @@ describe('VisionCamera - Video', () => {
}
})

it('records to a custom file path', async () => {
const session = await VisionCamera.createCameraSession(false)
const videoOutput = VisionCamera.createVideoOutput({
targetResolution: CommonResolutions.HD_16_9,
enableAudio: false,
})
await session.configure([
{
input: backDevice,
outputs: [{ output: videoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
await session.start()

// Discover the platform-specific app-writable temp directory by
// creating a default recorder and reading its chosen file path.
// We can't hard-code `/tmp/...` because iOS app sandboxes and Android
// app contexts both use platform-specific dynamic paths.
const probe = await videoOutput.createRecorder({})
const probePath = probe.filePath.replace(/^file:\/\//, '')
const tempDir = probePath.substring(0, probePath.lastIndexOf('/'))
const ext = Platform.OS === 'ios' ? 'mov' : 'mp4'
const customPath = `${tempDir}/visioncamera-custom-${Date.now()}.${ext}`

const recorder = await videoOutput.createRecorder({ filePath: customPath })
expect(recorder.filePath).toContain(customPath)

let finishedPath: string | undefined
let recordingError: Error | undefined
try {
await recorder.startRecording(
(filePath) => {
finishedPath = filePath
},
(error) => {
recordingError = error
},
)
await sleep(800)
await recorder.stopRecording()
await waitUntil(() => finishedPath != null, { timeout: 10_000 })

expect(recordingError).toBe(undefined)
expect(finishedPath).toContain(customPath)
} finally {
await session.stop()
}
})

it('auto-creates parent directories for a nested custom file path', async () => {
const session = await VisionCamera.createCameraSession(false)
const videoOutput = VisionCamera.createVideoOutput({
targetResolution: CommonResolutions.HD_16_9,
enableAudio: false,
})
await session.configure([
{
input: backDevice,
outputs: [{ output: videoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
await session.start()

const probe = await videoOutput.createRecorder({})
const probePath = probe.filePath.replace(/^file:\/\//, '')
const tempDir = probePath.substring(0, probePath.lastIndexOf('/'))
const ext = Platform.OS === 'ios' ? 'mov' : 'mp4'
// Use multiple non-existent nested folders so the test fails if the
// implementation doesn't recursively create parent dirs.
const customPath = `${tempDir}/visioncamera-nested-${Date.now()}/sub/dir/recording.${ext}`

const recorder = await videoOutput.createRecorder({ filePath: customPath })
expect(recorder.filePath).toContain(customPath)

let finishedPath: string | undefined
let recordingError: Error | undefined
try {
await recorder.startRecording(
(filePath) => {
finishedPath = filePath
},
(error) => {
recordingError = error
},
)
await sleep(800)
await recorder.stopRecording()
await waitUntil(() => finishedPath != null, { timeout: 10_000 })

// If the recording finished without error, the nested directories
// had to be created on the fly - otherwise the encoder couldn't
// have written any bytes.
expect(recordingError).toBe(undefined)
expect(finishedPath).toContain(customPath)
} finally {
await session.stop()
}
})

it('fails to record when given an unwritable file path', async () => {
const session = await VisionCamera.createCameraSession(false)
const videoOutput = VisionCamera.createVideoOutput({
targetResolution: CommonResolutions.HD_16_9,
enableAudio: false,
})
await session.configure([
{
input: backDevice,
outputs: [{ output: videoOutput, mirrorMode: 'auto' }],
constraints: [],
},
])
await session.start()

// The filesystem root is read-only inside both iOS and Android app
// sandboxes, so writing to `/<file>.mp4` must fail somehow.
const invalidPath = `/visioncamera-cannot-write-${Date.now()}.mp4`
let createError: Error | undefined
let startError: Error | undefined
let recordingError: Error | undefined

try {
let recorder: Recorder | undefined
try {
recorder = await videoOutput.createRecorder({ filePath: invalidPath })
} catch (e) {
createError = e as Error
}
if (recorder != null) {
try {
await recorder.startRecording(
() => {},
(error) => {
recordingError = error
},
)
} catch (e) {
startError = e as Error
}
// The error may surface synchronously (startRecording rejection)
// or asynchronously (onRecordingError callback). Wait briefly for
// the async case, but don't fail if the sync case already fired.
await waitUntil(() => startError != null || recordingError != null, {
timeout: 5_000,
}).catch(() => {
// Timed out without an error - the assertion below will fail.
})
}
} finally {
await session.stop()
}

expect(
createError != null || startError != null || recordingError != null,
).toBe(true)
})

it('returns supported video codecs on iOS after the output is attached', async () => {
if (Platform.OS !== 'ios') {
console.log('[SKIP] getSupportedVideoCodecs: iOS only')
Expand Down
Loading