Skip to content

updated with preview thumbnails#2597

Merged
gtsteffaniak merged 2 commits into
dev/v2.0.0from
plyr-preview-thumbnails
Jul 4, 2026
Merged

updated with preview thumbnails#2597
gtsteffaniak merged 2 commits into
dev/v2.0.0from
plyr-preview-thumbnails

Conversation

@gtsteffaniak

@gtsteffaniak gtsteffaniak commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Description

According to the contributing guide, A PR should contain:

  • A clear description of why it was opened.
  • A short title that best describes the change.
  • Must pass unit and integration tests, which can be run checked locally prior to opening a PR.
  • Any additional details for functionality not covered by tests.

Additional Details

Summary by CodeRabbit

  • New Features
    • Added a thumbnail-based scrub preview (with time display and loading feedback) for videos.
    • Added seek-on-release so seeking commits when the user lets go, reducing accidental jumps.
    • Enhanced shared-link/video playback support with query-based time + transcode handling.
    • Improved video playback integration with query-driven seeking and deferred media loading.
  • Bug Fixes
    • Improved buffering/loading UI responsiveness during playback and scrubbing.
    • Expanded media metadata extraction so files can display richer details (including codec/container info).

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Backend video metadata extraction now probes local media files for duration, codec, and container details with a duration fallback. Frontend adds shared playback query helpers, new Plyr scrub/seek/loading modules, and viewer wiring for deferred video loading and query-driven seeking.

Changes

Backend video metadata probing

Layer / File(s) Summary
FFmpeg probe and metadata extraction
backend/ffmpeg/metadata.go, backend/indexing/iteminfo/fileinfo.go, backend/adapters/fs/files/files.go, backend/ffmpeg/metadata_test.go
Adds local media path validation, ffprobe-based file probing, new media metadata fields, and probe-first video metadata extraction with tests for local path resolution.

Frontend Plyr playback UX

Layer / File(s) Summary
Playback query parsing and patch utilities
frontend/src/utils/playbackQuery.js, frontend/src/utils/playbackQuery.test.js
Adds playback time and transcode query parsing, formatting, patch construction, change detection, and tests for the new query helpers.
Plyr scrub preview module
frontend/src/plyr/plyrScrubPreview.js
Implements scrub preview popup creation, sizing, positioning, throttled fetches, caching, hover/scrub handling, and teardown.
Plyr seek-on-release module
frontend/src/plyr/plyrSeekOnRelease.js
Adds seek percent reading, release-based commit logic, duplicate change suppression, and seek-input blocking.
Plyr video loading indicator module
frontend/src/plyr/plyrVideoLoading.js
Tracks native video loading state from media events and exposes cleanup.
plyrViewer.vue wiring and view token lookup
frontend/src/views/files/plyrViewer.vue, frontend/src/views/files/ThreeJs.vue
Wires deferred video loading, query-based seeking, scrub preview, seek handling, loading overlay behavior, teardown updates, and the cached view token lookup change.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Files as extractVideoMetadata
  participant Ffmpeg as ffmpeg.Service
  participant Probe as ProbeStream

  Files->>Ffmpeg: ProbeFile(ctx, mediaPath)
  Ffmpeg->>Probe: ProbeStream(file, 30s timeout)
  Probe-->>Ffmpeg: duration, videoCodec, audioCodec, formatName
  Ffmpeg-->>Files: FileProbeInfo or error
  alt probe fails
    Files->>Ffmpeg: GetMediaDuration(localPath)
    Ffmpeg-->>Files: duration
  end
  Files->>Files: set item.Metadata when fields are present
Loading
sequenceDiagram
  participant Route as $route.query
  participant Viewer as plyrViewer.vue
  participant PlyrExt as scrub/seek/loading modules
  participant Player as Plyr player

  Route->>Viewer: query change (time/transcode)
  Viewer->>Viewer: buildPlaybackQueryPatch / playbackQueryChanged
  Viewer->>Player: apply seek on loadedmetadata
  Viewer->>PlyrExt: enablePlyrScrubPreview(player)
  Viewer->>PlyrExt: enablePlyrSeekOnRelease(player)
  Viewer->>PlyrExt: enablePlyrVideoLoadingIndicator(player, onLoadingChange)
  PlyrExt-->>Viewer: videoPlaybackLoading updates
  Viewer->>Viewer: destroyPlyr() tears down extensions
Loading

Possibly related PRs

Poem

I’m a rabbit with a viewport grin,
I hop where the previews begin.
Codec crumbs and timecodes gleam,
Scrub and seek in a bunny stream.
With loading hops and query cues,
I nibble bugs, then leave no clues. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and refers to a real part of the changeset: added preview thumbnail support in the Plyr video experience.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plyr-preview-thumbnails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
backend/ffmpeg/metadata.go (1)

21-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate duplicated Acquire/Release boilerplate.

ProbeFile repeats the exact nil guard + Acquire/defer Release pattern already present in GetMediaDuration (Line 49-66 in this file). Extracting a small helper would remove this duplication and keep the guard/lock lifecycle consistent across future ffmpeg service methods.

♻️ Proposed consolidation
+func (s *Service) withAcquired(ctx context.Context, fn func() error) error {
+	if s == nil || s.inner == nil {
+		return fmt.Errorf("ffmpeg service not available")
+	}
+	if err := s.Acquire(ctx); err != nil {
+		return err
+	}
+	defer s.Release()
+	return fn()
+}
+
 func (s *Service) ProbeFile(ctx context.Context, mediaPath string) (*FileProbeInfo, error) {
-	if s == nil || s.inner == nil {
-		return nil, fmt.Errorf("ffmpeg service not available")
-	}
-
-	if err := s.Acquire(ctx); err != nil {
-		return nil, err
-	}
-	defer s.Release()
-
-	info, err := s.inner.ProbeStream(ctx, goffmpeg.ProbeStreamOptions{
-		URL:        mediaPath,
-		StreamType: goffmpeg.StreamFile,
-		Timeout:    30 * time.Second,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &FileProbeInfo{
-		Duration:   info.Duration,
-		VideoCodec: info.VideoCodec,
-		AudioCodec: info.AudioCodec,
-		FormatName: info.FormatName,
-	}, nil
+	var result *FileProbeInfo
+	err := s.withAcquired(ctx, func() error {
+		info, err := s.inner.ProbeStream(ctx, goffmpeg.ProbeStreamOptions{
+			URL:        mediaPath,
+			StreamType: goffmpeg.StreamFile,
+			Timeout:    30 * time.Second,
+		})
+		if err != nil {
+			return err
+		}
+		result = &FileProbeInfo{
+			Duration:   info.Duration,
+			VideoCodec: info.VideoCodec,
+			AudioCodec: info.AudioCodec,
+			FormatName: info.FormatName,
+		}
+		return nil
+	})
+	return result, err
 }

As per path instructions, "reduce duplication where there is clear duplication found, use consolidated functions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/ffmpeg/metadata.go` around lines 21 - 46, `ProbeFile` duplicates the
same nil-check and `Acquire`/`defer Release` lifecycle already used by
`GetMediaDuration`; extract that shared ffmpeg service setup into a small helper
and have both methods use it so the guard/lock flow stays consistent. Keep the
helper tied to `Service` (for example around `Acquire`, `Release`, and the
nil/inner availability check) and update `ProbeFile` to focus only on
`ProbeStream` and building `FileProbeInfo`.

Source: Path instructions

backend/adapters/fs/files/files.go (1)

565-605: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for the probe-first fallback paths.
extractVideoMetadata has no coverage for probe success, duration fallback, or the dual-failure path; a small table-driven test in backend/adapters/fs/files/file_test.go would protect these branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/adapters/fs/files/files.go` around lines 565 - 605, Add table-driven
unit tests for extractVideoMetadata to cover the probe-first paths in files.go.
Exercise the case where FFmpegService.ProbeFile succeeds and populates
item.Metadata, the case where ProbeFile fails but GetMediaDuration succeeds and
sets only Duration, and the case where both fail and the original error is
returned. Use the extractVideoMetadata helper with a mocked ffmpeg.FFmpegService
(or test double) in file_test.go so these branches are locked down.

Source: Path instructions

frontend/src/utils/playbackQuery.js (2)

101-107: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

No finite-number guard in formatTimeForQuery.

If called with a non-finite seconds (e.g. NaN), this produces "NaN:NaN:NaN" instead of failing safely. All current call sites already guard with Number.isFinite before calling, so this isn't exploitable today, but it's a fragile implicit contract for a small, easily-testable utility.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/playbackQuery.js` around lines 101 - 107,
`formatTimeForQuery` currently assumes `seconds` is finite, which can produce
invalid output like `NaN:NaN:NaN` for bad inputs. Add an explicit
`Number.isFinite` guard at the start of `formatTimeForQuery` and make it fail
safely for non-finite values, while keeping the existing formatting logic
unchanged for valid numbers.

13-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify parsePlainSecondsString with existing regex pattern.

This manually walks characters to parse an integer/fraction, duplicating logic that's already expressed more simply elsewhere in the file (e.g. the /^\d+(\.\d+)?$/ regex + Number() pattern used in parseClockTimeString). Reusing that pattern here would be shorter and easier to maintain.

♻️ Suggested simplification
 function parsePlainSecondsString(str) {
-  if (str.includes(':')) {
-    return null;
-  }
-  let i = 0;
-  let intPart = 0;
-  while (i < str.length && str.charAt(i) >= '0' && str.charAt(i) <= '9') {
-    intPart = (intPart * 10) + (str.charCodeAt(i) - 48);
-    i += 1;
-  }
-  if (i === 0) {
-    return null;
-  }
-  let frac = 0;
-  let fracDiv = 1;
-  if (i < str.length && str.charAt(i) === '.') {
-    i += 1;
-    while (i < str.length && str.charAt(i) >= '0' && str.charAt(i) <= '9') {
-      frac = (frac * 10) + (str.charCodeAt(i) - 48);
-      fracDiv *= 10;
-      i += 1;
-    }
-  }
-  if (i !== str.length) {
-    return null;
-  }
-  return intPart + (frac / fracDiv);
+  if (!/^\d+(\.\d+)?$/.test(str)) {
+    return null;
+  }
+  const value = Number(str);
+  return Number.isFinite(value) ? value : null;
 }

The static-analysis "unsafe regex" warning on the similar pattern in parseClockTimeString (line 70) is a false positive here too — there's no nested/overlapping quantifier that could cause catastrophic backtracking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/playbackQuery.js` around lines 13 - 40, Simplify
parsePlainSecondsString by replacing the manual character-by-character parsing
with the same regex-plus-Number approach already used in parseClockTimeString.
Keep the existing validation behavior (reject strings with colons, non-numeric
input, or trailing junk) by checking the /^\d+(\.\d+)?$/ pattern before
converting. This change should be localized to parsePlainSecondsString in
playbackQuery.js and should preserve the current return values.
frontend/src/plyr/plyrScrubPreview.js (1)

148-161: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant branch in getScrubPreviewMount.

Both branches of the inner if (lines 152-155) return fsEl, so the condition checking container containment has no effect on the outcome.

♻️ Suggested simplification
 export function getScrubPreviewMount(player) {
   const fsEl = document.fullscreenElement ?? document.webkitFullscreenElement ?? null;
   const container = player?.elements?.container;
   if (fsEl instanceof HTMLElement) {
-    if (container instanceof HTMLElement && (fsEl === container || fsEl.contains(container))) {
-      return fsEl;
-    }
     return fsEl;
   }
   if (container instanceof HTMLElement && player?.fullscreen?.active) {
     return container;
   }
   return container instanceof HTMLElement ? container : document.body;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/plyr/plyrScrubPreview.js` around lines 148 - 161, Simplify
getScrubPreviewMount by removing the redundant inner conditional on container
containment, since both branches return fsEl. Keep the fullscreenElement lookup
and the player?.elements?.container / player?.fullscreen?.active fallback
behavior intact, but collapse the fsEl instanceof HTMLElement path in
plyrScrubPreview.js so the function’s return logic is clearer and has no no-op
branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/ffmpeg/metadata.go`:
- Around line 31-35: The ProbeStream call in the metadata lookup path currently
passes mediaPath directly without restricting allowed protocols, so the ffmpeg
probe can use default protocol handling on untrusted uploads. Update the
ProbeStream invocation in the metadata-reading flow to explicitly whitelist only
local file access (or otherwise sandbox the probe) using the existing
ProbeStreamOptions/ProbeStream call site so ProbeStream cannot open arbitrary
protocols from uploaded media.

In `@frontend/src/plyr/plyrScrubPreview.js`:
- Around line 83-99: The scrub percent logic is duplicated between
scrubPercentFromEvent and the local scrubPercentFromPointer inside
enablePlyrScrubPreview. Add an optional step parameter to scrubPercentFromEvent,
use it when calling quantizeScrubPercent, and replace the inner
scrubPercentFromPointer implementation with a call to scrubPercentFromEvent so
both paths share one source of truth.

---

Nitpick comments:
In `@backend/adapters/fs/files/files.go`:
- Around line 565-605: Add table-driven unit tests for extractVideoMetadata to
cover the probe-first paths in files.go. Exercise the case where
FFmpegService.ProbeFile succeeds and populates item.Metadata, the case where
ProbeFile fails but GetMediaDuration succeeds and sets only Duration, and the
case where both fail and the original error is returned. Use the
extractVideoMetadata helper with a mocked ffmpeg.FFmpegService (or test double)
in file_test.go so these branches are locked down.

In `@backend/ffmpeg/metadata.go`:
- Around line 21-46: `ProbeFile` duplicates the same nil-check and
`Acquire`/`defer Release` lifecycle already used by `GetMediaDuration`; extract
that shared ffmpeg service setup into a small helper and have both methods use
it so the guard/lock flow stays consistent. Keep the helper tied to `Service`
(for example around `Acquire`, `Release`, and the nil/inner availability check)
and update `ProbeFile` to focus only on `ProbeStream` and building
`FileProbeInfo`.

In `@frontend/src/plyr/plyrScrubPreview.js`:
- Around line 148-161: Simplify getScrubPreviewMount by removing the redundant
inner conditional on container containment, since both branches return fsEl.
Keep the fullscreenElement lookup and the player?.elements?.container /
player?.fullscreen?.active fallback behavior intact, but collapse the fsEl
instanceof HTMLElement path in plyrScrubPreview.js so the function’s return
logic is clearer and has no no-op branch.

In `@frontend/src/utils/playbackQuery.js`:
- Around line 101-107: `formatTimeForQuery` currently assumes `seconds` is
finite, which can produce invalid output like `NaN:NaN:NaN` for bad inputs. Add
an explicit `Number.isFinite` guard at the start of `formatTimeForQuery` and
make it fail safely for non-finite values, while keeping the existing formatting
logic unchanged for valid numbers.
- Around line 13-40: Simplify parsePlainSecondsString by replacing the manual
character-by-character parsing with the same regex-plus-Number approach already
used in parseClockTimeString. Keep the existing validation behavior (reject
strings with colons, non-numeric input, or trailing junk) by checking the
/^\d+(\.\d+)?$/ pattern before converting. This change should be localized to
parsePlainSecondsString in playbackQuery.js and should preserve the current
return values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4831fb78-98a0-4d84-82af-5d19f7a81e26

📥 Commits

Reviewing files that changed from the base of the PR and between a6a3fd1 and 15fd681.

⛔ Files ignored due to path filters (3)
  • backend/swagger/docs/docs.go is excluded by !backend/swagger/**
  • backend/swagger/docs/swagger.json is excluded by !backend/swagger/**
  • backend/swagger/docs/swagger.yaml is excluded by !backend/swagger/**
📒 Files selected for processing (9)
  • backend/adapters/fs/files/files.go
  • backend/ffmpeg/metadata.go
  • backend/indexing/iteminfo/fileinfo.go
  • frontend/src/plyr/plyrScrubPreview.js
  • frontend/src/plyr/plyrSeekOnRelease.js
  • frontend/src/plyr/plyrVideoLoading.js
  • frontend/src/utils/playbackQuery.js
  • frontend/src/utils/playbackQuery.test.js
  • frontend/src/views/files/plyrViewer.vue

Comment thread backend/ffmpeg/metadata.go
Comment thread frontend/src/plyr/plyrScrubPreview.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/utils/playbackQuery.js (1)

185-215: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize existing playback params before re-emitting them.

buildPlaybackQueryPatch() validates new patch values, but existing query.time and query.transcode are copied back as-is. An invalid current URL like ?transcode=foo can survive unrelated patches instead of being dropped/canonicalized.

Proposed fix
-  let timeValue = queryTime;
-  if (timeValue === undefined) {
-    const parsedTime = parsePlaybackTimeFromQuery(query);
-    timeValue = parsedTime === null ? undefined : formatTimeForQuery(parsedTime);
-  }
-  let transcodeValue = queryTranscode;
+  const parsedTime = parsePlaybackTimeFromQuery(query);
+  let timeValue = parsedTime === null ? undefined : formatTimeForQuery(parsedTime);
+  let transcodeValue = parseTranscodeModeFromQuery(query) ?? undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/playbackQuery.js` around lines 185 - 215,
buildPlaybackQueryPatch() only validates values coming from patch, but it reuses
existing queryTime and queryTranscode without normalization, so invalid URL
params can persist. Update the logic around queryTime/queryTranscode in
buildPlaybackQueryPatch() to parse/validate existing playback params the same
way as incoming patch values, and only emit canonical values via
formatTimeForQuery() and VALID_TRANSCODE_MODES; otherwise drop them from next.
🧹 Nitpick comments (1)
backend/ffmpeg/metadata_test.go (1)

19-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test case for the directory-rejection branch.

The table covers empty/remote/protocol/relative-path rejections but not the "media path is a directory" branch (metadata.go Lines 47-49), which is easy to add given the existing structure (t.TempDir() itself is a directory).

As per path instructions, "ensure functional features include new unit tests where its easily possible."

✅ Suggested additional test case
 		{name: "absolute local file", path: tmpPath},
+		{name: "directory path", path: t.TempDir(), wantErr: true},
 		{name: "empty path", path: "", wantErr: true},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/ffmpeg/metadata_test.go` around lines 19 - 30, Add a unit test case
to cover the directory-rejection branch in MetadataInfo; the current table in
metadata_test.go covers empty, remote, protocol, and relative paths but not when
the input is a directory. Use the existing MetadataInfo table-driven test and
add a case that passes a t.TempDir() value (or another directory path) with
wantErr true so the directory check in metadata.go is exercised.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/utils/playbackQuery.js`:
- Around line 185-215: buildPlaybackQueryPatch() only validates values coming
from patch, but it reuses existing queryTime and queryTranscode without
normalization, so invalid URL params can persist. Update the logic around
queryTime/queryTranscode in buildPlaybackQueryPatch() to parse/validate existing
playback params the same way as incoming patch values, and only emit canonical
values via formatTimeForQuery() and VALID_TRANSCODE_MODES; otherwise drop them
from next.

---

Nitpick comments:
In `@backend/ffmpeg/metadata_test.go`:
- Around line 19-30: Add a unit test case to cover the directory-rejection
branch in MetadataInfo; the current table in metadata_test.go covers empty,
remote, protocol, and relative paths but not when the input is a directory. Use
the existing MetadataInfo table-driven test and add a case that passes a
t.TempDir() value (or another directory path) with wantErr true so the directory
check in metadata.go is exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c999802-729e-457e-a144-d874c593dca7

📥 Commits

Reviewing files that changed from the base of the PR and between 15fd681 and 4021db6.

📒 Files selected for processing (5)
  • backend/ffmpeg/metadata.go
  • backend/ffmpeg/metadata_test.go
  • frontend/src/plyr/plyrScrubPreview.js
  • frontend/src/utils/playbackQuery.js
  • frontend/src/views/files/ThreeJs.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/plyr/plyrScrubPreview.js

@gtsteffaniak gtsteffaniak merged commit 35ae1b3 into dev/v2.0.0 Jul 4, 2026
22 checks passed
@gtsteffaniak gtsteffaniak deleted the plyr-preview-thumbnails branch July 4, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant