Skip to content
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
4 changes: 2 additions & 2 deletions public/demo-screen-recording/assets/introduction.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
This example demonstrates how to capture screen recordings for components where provenance tracking is difficult to implement. In this case, we illustrate the process using simple website components that are recorded directly from the screen.
This example demonstrates how to capture screen and webcam recordings for components where provenance tracking is difficult to implement. In this case, we illustrate the process using simple website components that are recorded directly from the screen and webcam.

## Relevant files:
* [The Config](https://github.com/revisit-studies/study/blob/main/public/demo-screen-recording/config.json)
* [bar-chart.html](https://github.com/revisit-studies/study/blob/main/public/demo-screen-recording/assets/bar-chart.html)

## Relevant documentation:
* [Recording Screen and Audio](https://revisit.dev/docs/designing-studies/record-screen/)
* [Recording Screen, Webcam, and Audio](https://revisit.dev/docs/designing-studies/record-screen/)
12 changes: 8 additions & 4 deletions public/demo-screen-recording/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"The reVISit Team"
],
"date": "2026-02-23",
"description": "A simple demo of using screen recording on stimuli that render an external website, where provenance tracking is difficult to implement.",
"description": "A simple demo of using screen and webcam recording on stimuli that render an external website, where provenance tracking is difficult to implement.",
"organizations": [
"University of Utah",
"WPI"
Expand All @@ -23,6 +23,7 @@
"windowEventDebounceTime": 200,
"recordAudio": true,
"recordScreen": true,
"recordWebcam": true,
"recordScreenFPS": 30
},
"importedLibraries": [
Expand All @@ -40,15 +41,17 @@
"response": [],
"instruction": "On this page, only audio recording is enabled.",
"recordAudio": true,
"recordScreen": false
"recordScreen": false,
"recordWebcam": false
},
"external_website_audio_screen": {
"type": "website",
"path": "https://www.revisit.dev",
"response": [],
"instruction": "On this page, screen recording is enabled, and audio recording is only enabled while clicking and holding the mic icon.",
"instruction": "On this page, screen and webcam recording are enabled, and audio recording is only enabled while clicking and holding the mic icon.",
"recordAudio": true,
"recordScreen": true,
"recordWebcam": true,
"clickToRecord": true
},
"barChart_audio_screen": {
Expand All @@ -58,7 +61,8 @@
"difficulty": "hard"
},
"description": "Question that asks users to count the number of bars that have a value greater than 1.",
"instruction": "How many bars have a value greater than 1? (Both audio and screen recording are enabled on this page)",
"instruction": "How many bars have a value greater than 1? (Audio, screen, and webcam recording are enabled on this page)",
"recordWebcam": true,
"path": "demo-screen-recording/assets/bar-chart.html",
"response": [
{
Expand Down
5 changes: 5 additions & 0 deletions public/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"library-quis",
"library-sam",
"library-screen-recording",
"library-webcam-recording",
"library-smeq",
"library-sus",
"library-ueq",
Expand Down Expand Up @@ -223,6 +224,10 @@
"path": "library-screen-recording/config.json",
"test": true
},
"library-webcam-recording": {
"path": "library-webcam-recording/config.json",
"test": true
},
"library-smeq": {
"path": "library-smeq/config.json",
"test": true
Expand Down
3 changes: 2 additions & 1 deletion public/libraries/screen-recording/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.4.3/src/parser/LibraryConfigSchema.json",
"description": "This is a required library for screen recording. It provides a component that requests user permission for screen and microphone access. Then, it starts screen capture until the end of the study.",
"description": "This library requests permission for screen recording and any configured webcam or microphone recording, then keeps those capture streams active until the end of the study.",
"components": {
"screenRecordingPermission": {
"description": "Get permission to start Screen recording",
Expand All @@ -9,6 +9,7 @@
"nextButtonLocation": "belowStimulus",
"nextButtonText": "Continue",
"recordScreen": true,
"clickToRecord": false,
"response": [
{
"hidden": true,
Expand Down
22 changes: 22 additions & 0 deletions public/libraries/webcam-recording/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/dev/src/parser/LibraryConfigSchema.json",
"description": "This library requests webcam and optional microphone permission, then keeps those capture streams active until the end of the study.",
"components": {
"webcamRecordingPermission": {
"description": "Get permission to start webcam recording",
"type": "react-component",
"path": "libraries/webcam-recording/assets/WebcamRecording.tsx",
"nextButtonLocation": "belowStimulus",
"nextButtonText": "Continue",
"recordAudio": false,
"clickToRecord": false,
"response": [{
"hidden": true,
"type": "reactive",
"id": "webcamRecordingPermission",
"prompt": "Webcam recording enabled"
}]
}
},
"sequences": {}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

# Demo Page

Any stimulus that follows the `screenRecordingPermission` page can be recorded.
Any stimulus that follows the `screenRecordingPermission` page can capture both screen and webcam video.

Explore the webpage in the iframe below, then proceed to the next page.

<iframe src="https://revisit.dev/" height="600px" style="border: 2px solid #000" >

2 changes: 1 addition & 1 deletion public/library-screen-recording/assets/screen-recording.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a demo of the library `screen-recording`.

This is a required library for screen recording. It provides a component that requests user permission for screen and microphone access. Then, it starts screen capture until the end of the study.
This library requests permission for screen recording and any configured webcam or microphone recording, then keeps those capture streams active until the end of the study.

## Available Components

Expand Down
7 changes: 4 additions & 3 deletions public/library-screen-recording/config.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.4.3/src/parser/StudyConfigSchema.json",
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/dev/src/parser/StudyConfigSchema.json",
"studyMetadata": {
"title": "Screen Recording",
"version": "1.0.0",
"authors": [
"The reVISit Team"
],
"date": "2026-02-23",
"description": "Example study using the screen-recording library.",
"description": "Example study using the screen-recording library with screen and webcam capture.",
"organizations": [
"University of Utah",
"WPI"
Expand All @@ -18,7 +18,8 @@
"logoPath": "revisitAssets/revisitLogoSquare.svg",
"withProgressBar": true,
"withSidebar": false,
"recordScreen": true
"recordScreen": true,
"recordWebcam": true
},
"importedLibraries": [
"screen-recording"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Demo Page

Any stimulus that follows the `webcamRecordingPermission` page can capture webcam video.

Proceed to the next page after confirming that your webcam preview is active.
13 changes: 13 additions & 0 deletions public/library-webcam-recording/assets/webcam-recording.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# WCR: Webcam Recording

This is a demo of the library `webcam-recording`.

This library requests webcam and optional microphone permission, then keeps those capture streams active until the end of the study.

## Available Components

- webcamRecordingPermission

## Available Sequences

None
46 changes: 46 additions & 0 deletions public/library-webcam-recording/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
Comment thread
JackWilb marked this conversation as resolved.
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/dev/src/parser/StudyConfigSchema.json",
"studyMetadata": {
"title": "WCR: Webcam Recording",
"version": "1.0.0",
"authors": [
"The reVISit Team"
],
"date": "2026-08-24",
"description": "Example study using the webcam-recording library.",
"organizations": [
"University of Utah",
"WPI"
]
},
"uiConfig": {
"contactEmail": "contact@revisit.dev",
"logoPath": "revisitAssets/revisitLogoSquare.svg",
"withProgressBar": true,
"withSidebar": false,
"recordWebcam": true
},
"importedLibraries": [
"webcam-recording"
],
"components": {
"introduction": {
"type": "markdown",
"path": "library-webcam-recording/assets/webcam-recording.md",
"response": []
},
"demo-recording": {
"type": "markdown",
"path": "library-webcam-recording/assets/webcam-recording-page.md",
"response": []
}
},
"sequence": {
"order": "fixed",
"components": [
"introduction",
"$webcam-recording.components.webcamRecordingPermission",
"demo-recording"
]
}
}
3 changes: 2 additions & 1 deletion src/analysis/individualStudy/StudyAnalysisTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function StudyAnalysisTabs({ globalConfig }: { globalConfig: GlobalConfig
const [selectedConditions, setSelectedConditions] = useState<string[]>(['ALL']);
const [availableConditions, setAvailableConditions] = useState<{ value: string; label: string }[]>([]);

const { hasAudioRecording, hasScreenRecording } = useStudyRecordings(studyConfig);
const { hasAudioRecording, hasScreenRecording, hasWebcamRecording } = useStudyRecordings(studyConfig);

const { storageEngine } = useStorageEngine();
const navigate = useNavigate();
Expand Down Expand Up @@ -441,6 +441,7 @@ export function StudyAnalysisTabs({ globalConfig }: { globalConfig: GlobalConfig
gap="10px"
hasAudio={hasAudioRecording}
hasScreenRecording={hasScreenRecording}
hasWebcamRecording={hasWebcamRecording}
/>
)}
</Flex>
Expand Down
3 changes: 2 additions & 1 deletion src/analysis/individualStudy/replay/AllTasksTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export function AllTasksTimeline({
const answerStatus = getComponentAnswerStatus(answer, correctAnswers, resolvedComponent?.response);
const hasAudio = resolvedComponent?.recordAudio ?? studyConfig?.uiConfig?.recordAudio ?? false;
const hasScreenRecording = resolvedComponent?.recordScreen ?? studyConfig?.uiConfig?.recordScreen ?? false;
const hasWebcamRecording = resolvedComponent?.recordWebcam ?? studyConfig?.uiConfig?.recordWebcam ?? false;

return {
identifier,
Expand Down Expand Up @@ -246,7 +247,7 @@ export function AllTasksTimeline({
)}
>
<g>
<SingleTask incomplete={answer.startTime === 0} answerStatus={answerStatus} hasAudio={hasAudio} hasScreenRecording={hasScreenRecording} key={identifier} labelHeight={currentHeight * LABEL_GAP} height={maxHeight} identifier={identifier} xScale={scale} scaleStart={scaleStart} scaleEnd={scaleEnd} trialOrder={answer.trialOrder} participantId={participantData.participantId} studyId={studyId} condition={conditionParam} isHovered={hoveredTaskIdentifier === identifier} isDimmed={hoveredTaskIdentifier !== null && hoveredTaskIdentifier !== identifier} onHover={() => setHoveredTaskIdentifier(identifier)} onHoverEnd={() => setHoveredTaskIdentifier(null)} />
<SingleTask incomplete={answer.startTime === 0} answerStatus={answerStatus} hasAudio={hasAudio} hasScreenRecording={hasScreenRecording} hasWebcamRecording={hasWebcamRecording} key={identifier} labelHeight={currentHeight * LABEL_GAP} height={maxHeight} identifier={identifier} xScale={scale} scaleStart={scaleStart} scaleEnd={scaleEnd} trialOrder={answer.trialOrder} participantId={participantData.participantId} studyId={studyId} condition={conditionParam} isHovered={hoveredTaskIdentifier === identifier} isDimmed={hoveredTaskIdentifier !== null && hoveredTaskIdentifier !== identifier} onHover={() => setHoveredTaskIdentifier(identifier)} onHoverEnd={() => setHoveredTaskIdentifier(null)} />
</g>
</Tooltip>),
};
Expand Down
12 changes: 10 additions & 2 deletions src/analysis/individualStudy/replay/SingleTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as d3 from 'd3';

import { useResizeObserver } from '@mantine/hooks';
import {
IconCheck, IconDeviceDesktop, IconMicrophone, IconProgress, IconX,
IconCamera, IconCheck, IconDeviceDesktop, IconMicrophone, IconProgress, IconX,
} from '@tabler/icons-react';
import { useNavigateToTrial } from '../../../utils/useNavigateToTrial';
import type { ComponentAnswerStatus } from '../../../utils/correctAnswer';
Expand Down Expand Up @@ -48,6 +48,7 @@ export function SingleTask({
answerStatus,
hasAudio,
hasScreenRecording,
hasWebcamRecording,
scaleStart,
scaleEnd,
incomplete,
Expand All @@ -67,6 +68,7 @@ export function SingleTask({
answerStatus: ComponentAnswerStatus | null,
hasAudio: boolean,
hasScreenRecording: boolean,
hasWebcamRecording: boolean,
scaleStart: number,
scaleEnd: number,
incomplete: boolean,
Expand All @@ -82,7 +84,7 @@ export function SingleTask({
const [ref, { width: labelWidth }] = useResizeObserver();

const navigateToTrial = useNavigateToTrial();
const iconCount = (incomplete || answerStatus ? 1 : 0) + (hasAudio ? 1 : 0) + (hasScreenRecording ? 1 : 0);
const iconCount = (incomplete || answerStatus ? 1 : 0) + (hasAudio ? 1 : 0) + (hasScreenRecording ? 1 : 0) + (hasWebcamRecording ? 1 : 0);
const iconsWidth = iconCount * (ICON_SIZE + ICON_GAP);
const labelOpacity = isDimmed ? 0.35 : 1;

Expand Down Expand Up @@ -165,6 +167,12 @@ export function SingleTask({
size="14"
/>
)}
{hasWebcamRecording && (
<IconCamera
color="orange"
size="14"
/>
)}
{incomplete ? (
<IconProgress
color="var(--mantine-color-orange-text)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { StudyConfig } from '../../../../parser/types';
import { ParticipantData } from '../../../../storage/types';
import type { StoredAnswer } from '../../../../store/types';
import { createMockStudyConfig } from '../../../tests/testUtils';
import { makeStoredAnswer, makeParticipant as _makeParticipant } from '../../../../tests/utils';
import { makeStudyConfig, makeStoredAnswer, makeParticipant as _makeParticipant } from '../../../../tests/utils';
import { AllTasksTimeline } from '../AllTasksTimeline';
import { SingleTask } from '../SingleTask';
import { SingleTaskLabelLines } from '../SingleTaskLabelLines';
Expand Down Expand Up @@ -37,6 +37,7 @@ vi.mock('@tabler/icons-react', () => ({
'aria-label'?: string;
color?: string;
}) => <span aria-label={ariaLabel} data-color={color}>icon-check</span>,
IconCamera: () => <span>icon-camera</span>,
IconMicrophone: () => <span>icon-microphone</span>,
IconProgress: () => <span>icon-progress</span>,
IconX: () => <span>icon-x</span>,
Expand Down Expand Up @@ -126,6 +127,7 @@ describe('SingleTask', () => {
answerStatus: null,
hasAudio: false,
hasScreenRecording: false,
hasWebcamRecording: false,
};

test('renders task name', () => {
Expand Down Expand Up @@ -166,6 +168,13 @@ describe('SingleTask', () => {
expect(html).toContain('icon-microphone');
});

test('shows camera icon when hasWebcamRecording', () => {
const html = renderToStaticMarkup(
<svg><SingleTask {...baseProps} hasWebcamRecording /></svg>,
);
expect(html).toContain('icon-camera');
});

test('shows progress icon when incomplete', () => {
const html = renderToStaticMarkup(
<svg><SingleTask {...baseProps} incomplete /></svg>,
Expand Down Expand Up @@ -210,6 +219,19 @@ describe('AllTasksTimeline', () => {
expect(html).toContain('trial1_0');
});

test('shows the webcam icon for a webcam-recorded task', () => {
const html = renderToStaticMarkup(
<AllTasksTimeline
participantData={makeParticipant()}
width={600}
studyId="test-study"
studyConfig={makeStudyConfig({ components: { trial1: { recordWebcam: true } } })}
maxLength={undefined}
/>,
);
expect(html).toContain('icon-camera');
});

test('shows an unknown indicator and tooltip value for an answer without configured correctness', () => {
const participant = makeParticipant({
answers: {
Expand Down
Loading
Loading