Skip to content

Add PDMediaPlayer component - #245

Merged
davidnmbond merged 9 commits into
mainfrom
feature/pdmediaplayer
Oct 15, 2026
Merged

davidnmbond merged 9 commits into
mainfrom
feature/pdmediaplayer

Conversation

@davidnmbond

Copy link
Copy Markdown
Contributor

Summary

Adds PDMediaPlayer, a generic, domain-neutral player for long recordings: audio (the main use) or video through the browser's native media element, with playback speed control and markers (events along the timeline) that can be filtered by kind and jumped between.

The element streams its source itself, so a server with HTTP range requests lets the user seek anywhere without downloading the whole file. The player does no buffering of its own.

Public API

Media: Src, MediaKind (Audio/Video), Poster, Title, Preload (MediaPreload), Autoplay (false), Loop, Muted/MutedChanged, Volume/VolumeChanged, ShowDownload, DownloadUrl, DownloadFileName.

Transport: SkipBackSeconds (15), SkipForwardSeconds (30), PlaybackRates (0.75 to 2 by default; values outside 0.5 to 4 dropped), PlaybackRate/PlaybackRateChanged (pitch preserved; kept across Src changes), Position/PositionChanged, StartAt, TimeUpdateRate (4 a second), ShowErrors.

Persistence: PersistKey and PersistInterval (5 s). These remember the speed, and the position per source (the 50 most recent), in local storage. The position is cleared at the end. A cascaded IAsyncStateManager (such as PDLocalStorageStateManager) is used when there is one.

Range: RangeStart and RangeEnd shade part of the seek bar and raise OnRangeEntered and OnRangeLeft. The audio is never trimmed.

Markers:

  • Markers (IEnumerable<PDMediaMarker>, in any order). Each PDMediaMarker has Id, Position, optional Length (a span), Kind, Label, Detail, CssClass and Color.
  • SeekLead (0) and MarkerTolerance (0.5 s).
  • VisibleKinds/VisibleKindsChanged, KindLabels and KindColors.
  • ShowKindFilter (toggle chips with counts), ShowMarkerList, MarkerListExpanded, MarkerListPlacement (Below/Side), MarkerListMaxHeight and MarkerTickResolution (400).

Methods. All are safe to call before the media is ready: a seek waits for the media to load, and a play request waits for the script.

  • PlayAsync, PauseAsync, TogglePlayAsync
  • SeekAsync(TimeSpan), SkipAsync(TimeSpan)
  • SeekToMarkerAsync(marker)
  • NextMarkerAsync(kind?), PreviousMarkerAsync(kind?): these return the marker, or null.
  • SetPlaybackRateAsync, SetVolumeAsync, SetMutedAsync, ToggleMuteAsync
  • SetKindVisibleAsync, IsKindVisible

Events: OnReady (the duration, or null for a live stream), OnTimeUpdate, OnPlay, OnPause, OnEnded, OnError and OnMarkerSelected.

  • OnError passes a MediaPlayerError with a MediaErrorKind (Unauthorized, Forbidden, NotFound, ServerError, Network, Decode, NotSupported, Aborted or Unknown) and a friendly message.

Read-only properties: CurrentPosition, Duration, IsReady, IsLive, IsPlaying, CurrentPlaybackRate, CurrentVolume, IsMuted, LastError, CurrentMarker, SortedMarkers, VisibleMarkers and AvailablePlaybackRates.

Public helpers in PanoramicData.Blazor.Helpers: MediaTimeFormatter, MediaPlaybackRates and MediaMarkerNavigator.

No existing public member was changed.

Behaviour notes

  • Keyboard. The shortcuts work only while focus is inside the player, never across the whole page:

    • K plays or pauses.
    • J or [ goes to the previous marker, and L or ] to the next.
    • M mutes.
    • < and > change the speed.

    The speed menu stops key presses from reaching the player, and presses with Ctrl, Alt or Meta, or held-down repeats, are ignored. The seek bar is a native input type=range with aria-valuetext (for example "30 minutes of 1 hour"), so arrow keys, Page Up/Down, Home and End and screen readers work natively.

  • Marker navigation counts from the position plus SeekLead, so after jumping to a marker (minus its lead), "next" moves on rather than finding it again. Previous returns to the start of the current marker when more than MarkerTolerance into it, and otherwise goes to the marker before. Without a kind, only visible kinds count; asking for a kind ignores the filter. Markers past the end are drawn at the end and are not found again once there.

  • Many markers. The list uses Virtualize. On the seek bar, the bar is split into MarkerTickResolution slots, and markers of one kind (and colour and class) in the same slot, or overlapping a span already drawn, merge into one tick whose tooltip gives the count. So at most that many ticks are drawn per kind. Lookups use binary search.

  • Errors. A media element hides the HTTP status. On a network or format error, the script asks the server for byte 0 of the source (Range: bytes=0-0) to tell 401, 403 and 404 apart from a genuinely unplayable file.

  • Live and unknown durations. An unknown (NaN) duration waits for a later durationchange. An infinite one (live) disables seeking and skipping.

  • Parameter echoes. A parameter is applied only when the host passes a different value from last time, so a host that re-renders does not undo the user's speed, volume or position. A bound Position passing back a recently reported value does not seek.

  • Script. PDMediaPlayer.razor.js only attaches listeners, forwards a state snapshot (throttled for timeupdate and progress, with a trailing send), applies commands, and detaches on dispose. Every decision is in C#: MediaTimelineLayout, MediaErrorMapper and MediaKeyboard are internal; the others are public.

Tests

  • bUnit and unit tests: 229 new, in PDMediaPlayerTests*.cs, Helpers/Media*Tests.cs and Models/MediaPlayerPersistedStateTests.cs. The script is mocked through bUnit's JS interop.
  • Full suite: 4030 total, 0 failed, 0 skipped.
  • Playwright e2e: tests/pdmediaplayer.spec.ts, which runs with the repo's existing Playwright setup. It has 5 tests in Chromium covering play, speed (and preservesPitch), slider seek, the marker buttons, J, L, [ and ], the list, the kind filter, focus-scoped shortcuts and detaching on navigation. All 15 runs passed with --repeat-each=3.

Coverage of the new library files

Measured with Lines Branches
Repo coverage.config (full suite) 740/740 (100%) 433/452 (95.8%)
coverage.config without the CompilerGenerated exclusion, so async state machines count 1231/1231 (100%) 635/659 (96.4%)

The uncovered branches are compiler-generated string-switch branches in MediaKeyboard, plus a few || short circuits.

Demo

/pdmediaplayer, added to the NavMenu, with a Documentation tab. It plays a one minute WAV synthesised by the checked-in PanoramicData.Blazor.Demo/Data/New-DemoShowAudio.ps1. The audio contains no third-party material and is about 480 KB. The demo has:

  • markers of three kinds and a shaded range
  • a switch to 5,009 markers
  • the existing TestVideo.webm as a video source
  • a missing file, to show the 404 message

ComponentDocumentation.md gains only the generated PDMediaPlayer section. The rest of the generator's output (drift in other components) was left out.

Not verified

Not tested: a screen reader, Firefox, Safari, real phones and touch devices, the WebAssembly host, or dark-theme colours (checked by eye in light theme only). The phone layout was checked at 390 px in headless Chromium.

🤖 Generated with Claude Code

davidnmbond and others added 3 commits October 15, 2026 15:48
A generic audio and video player built on the native media element, with an
accessible Bootstrap transport (play, skip, seek slider with buffered ranges,
time, mute, volume, speed menu and optional download), playback speed from
0.5x to 4x with pitch preserved, markers drawn on the seek bar and listed in a
virtualised, kind-filterable list, previous and next marker navigation with a
seek lead, an optional shaded range with crossing events, friendly error
reporting (401, 403, 404, network, decode) and optional persistence of the
speed and position.

All decisions live in C# helpers (MediaTimeFormatter, MediaPlaybackRates,
MediaMarkerNavigator, MediaTimelineLayout, MediaErrorMapper, MediaKeyboard);
the script only forwards element events and applies commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bUnit tests for the component (rendering, media events, playback methods,
markers, kind filter, navigation with tolerance and seek lead, keyboard
shortcuts, range crossings, error mapping with the HTTP status probe,
persistence through local storage or a cascaded state manager, source and
kind changes, live streams, large marker sets and disposal), with the script
mocked through bUnit's JS interop, and unit tests for the helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo plays a one minute WAV synthesised by Data/New-DemoShowAudio.ps1
(no third-party audio, so no licence beyond the repository's own; about
480 KB, served by the static files middleware with range requests), with
markers of three kinds, a shaded range, a switch to 5,000 generated markers,
the existing TestVideo.webm as a video source and a missing file to show
error reporting. The Documentation tab covers the API, keyboard shortcuts and
how large marker sets are drawn. tests/pdmediaplayer.spec.ts checks the
script in Chromium (play, speed with pitch preserved, seek, marker buttons,
shortcuts, kind filter, focus scoping and detaching on navigation).
ComponentDocumentation.md gains only the generated PDMediaPlayer section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 932 complexity · 2 duplication

Metric Results
Complexity 932
Duplication 2

View in Codacy

🟢 Coverage 100.00% diff coverage · +0.01% coverage variation

Metric Results
Coverage variation ✅ +0.01% coverage variation
Diff coverage ✅ 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (1b4b7bc) 16056 16033 99.86%
Head commit (f6b167b) 17448 (+1392) 17425 (+1392) 99.87% (+0.01%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#245) 1158 1158 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

AI Reviewer: run a review on demand. To trigger the first review automatically, go to your organization or repository integration settings. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

davidnmbond and others added 6 commits October 15, 2026 16:30
- Split the methods Lizard found too complex (parameter application, marker
  parameters, media event dispatch, metadata handling, position saving).
- Media events dispatch through a table of handlers instead of a switch.
- Declare the interop fields where they are assigned, and expose the media
  element as a property set by @ref, as PDTimeline does.
- Replace optional parameters on NextMarkerAsync and PreviousMarkerAsync with
  overloads, add a default to the shortcut switch, and make the rate limits
  static read-only properties.
- Script: format with Prettier, use Date.now, ask for the HTTP status with
  XMLHttpRequest, and return play's promise without a try block.
- Stylesheet: order rules by specificity and separate custom properties.
- Tests and demo: deterministic spread instead of Random, no redundant
  arguments or unused locals, demo state as properties, named parameters in
  the audio script (it still writes byte-identical audio).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status probe no longer constructs a Promise: the script sends the request
and answers through OnProbeResult, and the error waits for that answer. A
result for a replaced source, or after disposal, is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Segments are stretches of the timeline with a start and an end, such as
songs, talking sections or adverts. They may overlap and leave gaps.

- PDMediaSegment (Id, Start, End, Kind, Label, Detail, Text, Lines, Lane,
  CssClass, Color, Confidence, Source) and PDMediaTextLine for timed
  transcript lines.
- PDMediaSegment.FromMarkers derives segments marker to marker, with options
  to skip kinds, end a segment with an end kind and merge neighbours of the
  same kind.
- Segments are drawn as labelled bars in lanes beneath or above the seek bar,
  with overlaps packed into lanes (MaxSegmentLanes) and short neighbours
  merged per slot so thousands stay cheap. A virtualised, kind-filterable
  segment list shows the time range and length.
- A now playing strip shows the current segment, plays it on its own
  (pausing at its end) or loops it, and opens a transcript pane that
  highlights the line being spoken, keeps it in view and seeks on selection.
- NextSegmentAsync, PreviousSegmentAsync, SeekToSegmentAsync,
  PlaySegmentAsync, SetSegmentLoopAsync and SetSegmentKindVisibleAsync;
  OnSegmentSelected, OnSegmentEntered and OnSegmentLeft; shortcuts comma,
  full stop and P.

Also: keyboard shortcuts and kind counting are table driven and shared, and
a kind filter chosen with the chips now survives an unbound host
re-rendering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmit

PDMediaPlayer.Segments.cs holds the segment methods, parameters handling,
crossing detection and play-once logic used by the previous commit, which
does not build without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Separate the side-placement comment from the rule before it, and hold the
demo page's player reference in a property set by @ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sonar cannot see the @ref assignment of a private property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidnmbond
davidnmbond merged commit 42f6ca5 into main Oct 15, 2026
8 checks passed
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