Repository navigation
Add PDMediaPlayer component - #245
Merged
Merged
Conversation
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>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 932 |
| Duplication | 2 |
🟢 Coverage 100.00% diff coverage · +0.01% coverage variation
Metric Results Coverage variation ✅ +0.01% coverage variation Diff coverage ✅ 100.00% diff coverage 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.
TIP This summary will be updated as you push new changes.
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 acrossSrcchanges),Position/PositionChanged,StartAt,TimeUpdateRate(4 a second),ShowErrors.Persistence:
PersistKeyandPersistInterval(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 cascadedIAsyncStateManager(such asPDLocalStorageStateManager) is used when there is one.Range:
RangeStartandRangeEndshade part of the seek bar and raiseOnRangeEnteredandOnRangeLeft. The audio is never trimmed.Markers:
Markers(IEnumerable<PDMediaMarker>, in any order). EachPDMediaMarkerhasId,Position, optionalLength(a span),Kind,Label,Detail,CssClassandColor.SeekLead(0) andMarkerTolerance(0.5 s).VisibleKinds/VisibleKindsChanged,KindLabelsandKindColors.ShowKindFilter(toggle chips with counts),ShowMarkerList,MarkerListExpanded,MarkerListPlacement(Below/Side),MarkerListMaxHeightandMarkerTickResolution(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,TogglePlayAsyncSeekAsync(TimeSpan),SkipAsync(TimeSpan)SeekToMarkerAsync(marker)NextMarkerAsync(kind?),PreviousMarkerAsync(kind?): these return the marker, or null.SetPlaybackRateAsync,SetVolumeAsync,SetMutedAsync,ToggleMuteAsyncSetKindVisibleAsync,IsKindVisibleEvents:
OnReady(the duration, or null for a live stream),OnTimeUpdate,OnPlay,OnPause,OnEnded,OnErrorandOnMarkerSelected.OnErrorpasses aMediaPlayerErrorwith aMediaErrorKind(Unauthorized,Forbidden,NotFound,ServerError,Network,Decode,NotSupported,AbortedorUnknown) and a friendly message.Read-only properties:
CurrentPosition,Duration,IsReady,IsLive,IsPlaying,CurrentPlaybackRate,CurrentVolume,IsMuted,LastError,CurrentMarker,SortedMarkers,VisibleMarkersandAvailablePlaybackRates.Public helpers in
PanoramicData.Blazor.Helpers:MediaTimeFormatter,MediaPlaybackRatesandMediaMarkerNavigator.No existing public member was changed.
Behaviour notes
Keyboard. The shortcuts work only while focus is inside the player, never across the whole page:
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=rangewitharia-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 thanMarkerToleranceinto 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 intoMarkerTickResolutionslots, 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
Positionpassing back a recently reported value does not seek.Script.
PDMediaPlayer.razor.jsonly attaches listeners, forwards a state snapshot (throttled fortimeupdateandprogress, with a trailing send), applies commands, and detaches on dispose. Every decision is in C#:MediaTimelineLayout,MediaErrorMapperandMediaKeyboardare internal; the others are public.Tests
PDMediaPlayerTests*.cs,Helpers/Media*Tests.csandModels/MediaPlayerPersistedStateTests.cs. The script is mocked through bUnit's JS interop.tests/pdmediaplayer.spec.ts, which runs with the repo's existing Playwright setup. It has 5 tests in Chromium covering play, speed (andpreservesPitch), 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
coverage.config(full suite)coverage.configwithout theCompilerGeneratedexclusion, so async state machines countThe 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-inPanoramicData.Blazor.Demo/Data/New-DemoShowAudio.ps1. The audio contains no third-party material and is about 480 KB. The demo has:TestVideo.webmas a video sourceComponentDocumentation.mdgains 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