Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f4ace61
test(e2e): cover playback that keeps going while browsing, with the v…
chhoumann Oct 11, 2026
e2fe17a
Merge remote-tracking branch 'origin/master' into feat/player-controller
chhoumann Oct 11, 2026
26da453
feat(player): add selectSource, the per-load media source rules
chhoumann Oct 11, 2026
737b4e7
feat(player): add PlayerController, one media element with a status m…
chhoumann Oct 11, 2026
444a80f
fix(ui): apply slider limits before its value
chhoumann Oct 11, 2026
4495749
fix(player): play through PlayerController and delete the playback st…
chhoumann Oct 11, 2026
9b58066
test(e2e): observe the player's own media element and assert the new …
chhoumann Oct 11, 2026
f432b39
docs: describe playback that outlives the view and the stricter seek …
chhoumann Oct 11, 2026
fea0561
fix(player): recover a video that moving to a popout reloads
chhoumann Oct 11, 2026
3221852
test(e2e): stop playback before the harness rolls data.json back
chhoumann Oct 11, 2026
8163027
test(player): cover consecutive intents on loaded media with no media…
chhoumann Oct 11, 2026
c42b1d1
fix(player): publish a resume at once so the next intent acts on it
chhoumann Oct 11, 2026
5954452
test(player): cover a reset that aborts a pending play
chhoumann Oct 11, 2026
d4fd5aa
fix(player): keep the play intent when a reset aborts a pending play
chhoumann Oct 11, 2026
da0bba1
test(player): cover a finished video that a reset reloads
chhoumann Oct 11, 2026
6f18202
fix(player): keep a finished episode finished when a reset reloads it…
chhoumann Oct 11, 2026
371830d
test(api): cover an undefined seek before the metadata and during a s…
chhoumann Oct 11, 2026
2e14355
fix(player): reject a seek to a time that is not a number
chhoumann Oct 11, 2026
e1fab9e
test(api): cover start() while a segment plays
chhoumann Oct 11, 2026
609e360
fix(player): leave a playing segment alone when resume() has nothing …
chhoumann Oct 11, 2026
b6637b8
test(player): cover a sync that lands while the episode is restored
chhoumann Oct 11, 2026
d89faa5
fix(player): show the selection and position a sync brings while noth…
chhoumann Oct 11, 2026
8a9f6e1
refactor: one rule for the url a PodNotes link carries, shared by lin…
chhoumann Oct 11, 2026
7da70ad
test(uri): cover a link's explicit vault path and a refused link duri…
chhoumann Oct 11, 2026
48ac71a
fix(uri): play the vault file a link names over a same-named registra…
chhoumann Oct 11, 2026
b290eea
fix(uri): defer a link's play only once the link passed its checks
chhoumann Oct 11, 2026
d81d67e
test(player): cover a video view in a popout and the video's accessib…
chhoumann Oct 11, 2026
0ad58fd
fix(player): show the artwork for a video in a popout, and name the v…
chhoumann Oct 11, 2026
d90a050
test(player): check a new load's position and length in the controlle…
chhoumann Oct 11, 2026
c629e4d
test: name the real ordering constraint of the player's save on unload
chhoumann Oct 11, 2026
58817fe
test(player): cover releasing the media of a dropped element
chhoumann Oct 11, 2026
c2ad36a
fix(player): release a media element's source before dropping it
chhoumann Oct 11, 2026
d5bfdaf
refactor(player): clearer names, one start-duration rule, no leaked t…
chhoumann Oct 11, 2026
bb22fee
docs(api): say what a forward skip near the end does
chhoumann Oct 11, 2026
14d6b26
docs: describe restore after a sync, video in a popout, and a link's …
chhoumann Oct 11, 2026
ba0cc7c
chore: format the files this round touched
chhoumann Oct 11, 2026
1433403
refactor(ui): name the player's remaining time
chhoumann Oct 11, 2026
43423b0
fix(ui): fit the artwork placeholder to its frame
chhoumann Oct 11, 2026
03fd720
docs(player): say when a sync reaches the restored episode
chhoumann Oct 11, 2026
2cca14d
test(player): cover a position moved before a sync, and a sync after …
chhoumann Oct 11, 2026
2490809
test(player): cover a sync after a failed load that followed a seek
chhoumann Oct 11, 2026
9e5e37a
fix(player): keep a position the user moved before loading, and start…
chhoumann Oct 11, 2026
8e1ad60
test(uri): cover a link to a file whose name a file of another type t…
chhoumann Oct 11, 2026
025a1cd
fix(uri): play a link's vault file as that file, not as the same-name…
chhoumann Oct 11, 2026
434127b
test(player): cover a pause the browser makes while media loads
chhoumann Oct 11, 2026
128bf2a
fix(player): keep a pause the browser makes while media loads
chhoumann Oct 11, 2026
5f274b5
docs: say a forward skip near the end keeps playing, so the episode f…
chhoumann Oct 11, 2026
ad5484a
test(player): cover releasing a finished video that a reset reloaded
chhoumann Oct 11, 2026
3a0c73f
fix(player): let go of a finished video's media when a reset reloads it
chhoumann Oct 11, 2026
9d67156
test: stub jsdom's media load() where the plugin unloads its player
chhoumann Oct 11, 2026
0c8edfb
docs(api): say an over-skip lets a playing episode finish
chhoumann Oct 11, 2026
4f52fba
docs: say a position moved before playing stays through a sync
chhoumann Oct 11, 2026
7ca035f
test(player): cover a seek followed by a media error
chhoumann Oct 11, 2026
c30548e
fix(player): retry at the position the user sought after a media error
chhoumann Oct 11, 2026
a082d65
test(player): cover a link's time and a synced position after a media…
chhoumann Oct 11, 2026
95b3fd5
fix(player): treat a link's time as a move, and end a move once it is…
chhoumann Oct 11, 2026
7df32b4
test(player): cover a moved position across a failed retry
chhoumann Oct 11, 2026
ee30406
fix(player): keep a moved position until it is saved, across a failed…
chhoumann Oct 11, 2026
58b77ee
Merge remote-tracking branch 'origin/master' into feat/player-controller
chhoumann Oct 11, 2026
42e78a8
Merge commit '3e8db3a' into feat/player-controller
chhoumann Oct 11, 2026
989c184
test(player): cover intents that supersede a pending link, and legacy…
chhoumann Oct 11, 2026
2acb46a
fix(player): let a resume or a seek supersede a link that is still lo…
chhoumann Oct 11, 2026
87a8193
test(player): cover media-key intents and stale link notices after an…
chhoumann Oct 11, 2026
b648736
fix(player): let media keys and immediate links supersede a link that…
chhoumann Oct 11, 2026
d59d4b2
chore: format the URI handler tests
chhoumann Oct 12, 2026
4ee9c8a
test(player): pin the queue advance, a segment's own pause and a not-…
chhoumann Oct 12, 2026
3be6fab
test(uri): cover a link to a saved private feed whose URL is not on t…
chhoumann Oct 12, 2026
84422f8
test(uri): pin that a play during a private feed fetch wins
chhoumann Oct 12, 2026
8c9ea1a
fix(uri): let a private feed link this device cannot fetch cancel not…
chhoumann Oct 12, 2026
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
6 changes: 4 additions & 2 deletions .claude/skills/verify/features/player.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ Preconditions:

## Gotchas

- Clicking a finished episode resumes it at its end. That fires `ended` at once, and the queue auto-advances to another episode. Use unplayed episodes or a fresh vault.
- Playing a finished episode starts it over at 0.
- Audio keeps playing on the grid and list tabs, in settings, and with the view closed. The one media element is `audio.podnotes-media` (or `video.podnotes-media`) in the main window, not inside the view.
- After a restart the last episode shows paused at its saved position. Nothing loads or plays until you press play. A sync that lands before then updates the shown episode and position, unless you moved the position.
- A video plays in the main window only. With the view in a popout, the player shows the artwork and the video keeps playing hidden in the main window.
- With `Keep a queue of episodes you switch away from` on (the default), switching episodes queues the previous one, and it plays again when the current one ends.
- The skip buttons have no accessible name. Address them by position inside `.controls-container`.
- The playback-rate slider renders at 1.5 while the real rate is 1x. Assert on `api.playbackRate` or the rate label, not the slider.
- Headless Obsidian plays to a null audio sink. Prove playback through `api.currentTime` advancing, not by sound.
15 changes: 9 additions & 6 deletions docs/src/content/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,17 @@ Local episodes extend this type with `podcastName: "local file"`, empty `descrip

## `isPlaying`

Returns the inverse of PodNotes' paused state. It describes the requested player state rather than proof that media playback has started.
Returns `true` while the current episode plays, or loads to play. It is `false` for a paused, finished, or freshly restored episode.

## `length`

Returns the player duration in seconds. The value starts at zero before media metadata loads.
Returns the player duration in seconds. Before the media loads, it is the saved or feed-provided duration, or zero when neither is known.

## `currentTime`

Gets or sets the player position in seconds. Setting a position clears an active segment's end boundary.
Gets or sets the player position in seconds. Before the media loads, it is the position playback starts from.
Setting a position clears an active segment's end boundary. A value that is not a finite, non-negative number is ignored.
A position at or past the end stops 0.25 seconds before the end and pauses, so the episode does not finish.

## `volume`

Expand Down Expand Up @@ -162,11 +164,12 @@ Non-finite writes fall back to **Default Playback Rate**.
Changing the current rate does not change that saved default.

## Playback controls
`start()`, `stop()`, and `togglePlayback()` control the current episode.
`stop()` pauses without resetting the position.
`start()`, `stop()`, and `togglePlayback()` control the current episode, also while the PodNotes view is closed.
`stop()` pauses without resetting the position. `start()` on a finished episode starts it over.
`skipBackward()` and `skipForward()` use **Skip backward length (s)** and **Skip forward length (s)**.
Invalid skip lengths fall back to 15 seconds. Backward skips stop at zero.
With a known duration, forward skips stop up to 0.25 seconds before the end without moving backward.
With a known duration, a forward skip goes no further than 0.25 seconds before the end and never moves backward.
Playback continues, so a playing episode finishes and the queue advances.

`increasePlaybackRate()` and `decreasePlaybackRate()` adjust the current playback
speed in `0.1x` steps. `resetPlaybackRate()` returns playback to the default
Expand Down
3 changes: 2 additions & 1 deletion docs/src/content/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ It requires a loaded episode and works while paused. The default skip length is
## Skip Forward

`podnotes:skip-forward` seeks forward by **Skip forward length (s)**.
When duration is known, a forward skip stops up to 0.25 seconds before the end without rewinding an episode already nearer its end.
When duration is known, a forward skip goes no further than 0.25 seconds before the end and never rewinds an episode already nearer its end.
Playback continues, so a playing episode finishes and the queue advances.
It requires a loaded episode and works while paused. The default skip length is 15 seconds.

## Download Playing Episode
Expand Down
8 changes: 6 additions & 2 deletions docs/src/content/docs/podcasts.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ If you would rather manage the queue yourself, turn off **Keep a queue of episod
You can still add, remove, and reorder episodes in the queue manually from the episode context menu. While the queue is empty in this mode, its tile in the podcast grid and its list in the player are hidden.

## Player
The player will automatically load and play the current episode.
An episode keeps playing while you browse podcasts, change settings, or close the PodNotes view.
When Obsidian starts, the player shows the last episode paused at its saved position. Nothing plays until you press play.
If a sync brings newer progress or another episode before then, the player shows that instead.
A position you already moved that episode to stays.
A video plays in the main Obsidian window. When the PodNotes view is in a separate window, it shows the episode artwork.

Clicking on the epsiode image will toggle episode playback.
Clicking on the episode image will toggle episode playback.

The player also features a progress bar, which you can use to seek to a specific time in the episode.
You can also press the buttons on the player to skip backwards or forwards in the episode.
Expand Down
7 changes: 5 additions & 2 deletions docs/src/content/docs/timestamps.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,24 @@ The handler accepts these query parameters:
| --- | --- | --- |
| `url` | Yes | Podcast feed URL, vault media path, or protected private-feed placeholder. |
| `episodeName` | Yes | Episode title. |
| `time` | No | Start time parsed as a non-negative finite number of seconds. |
| `endTime` | No | Segment end parsed as a finite number of seconds greater than `time`. Requires a start time. |
| `time` | No | Start time in seconds. The whole value must be a non-negative finite number, so `12abc` is refused. |
| `endTime` | No | Segment end in seconds, greater than `time`. The whole value must be a finite number. Requires a start time. |
| `end` | No | Accepted alias of `endTime`. |
| `to` | No | Accepted alias of `endTime`, after `end` in precedence. |

An omitted or empty `time` resumes from saved progress.
An unplayed or finished episode starts at zero.
A `time` at or past the episode's end stops 0.25 seconds before the end and pauses.
`endTime` takes precedence over `end`, which takes precedence over `to`.

Query values are percent-encoded. Current links encode spaces as `%20` and literal plus signs as `%2B`.
Older links using `+` for spaces are also tried during title and local-path lookup.

Feed targets must use a public HTTP or HTTPS URL. A protected private-feed placeholder resolves only through a saved subscription and its device-local secret.
Local targets need a registered episode and a media file. See [Sync and devices](/sync/#removal-and-existing-links).
When the vault path in `url` exists, that file plays, even if a same-named file in another folder took over its registration.

A link plays the loaded episode without a feed lookup only when its `url` names that episode's feed, or its file for a local file.
Episode lookup first tries the title, ignoring case and surrounding whitespace.
Its fallback requires a unique match with the same normalized word set, ignoring punctuation, word order, and leading zeros in numeric words.
Different words or episode numbers can prevent a match. Timestamp links do not guarantee recovery after every feed title change.
Loading
Loading